Skip to content

Migrate cross-repo CLAUDE.md sections to workspace pointers#210

Open
bernardladenthin wants to merge 31 commits into
mainfrom
claude/vigilant-gauss-7jXfW
Open

Migrate cross-repo CLAUDE.md sections to workspace pointers#210
bernardladenthin wants to merge 31 commits into
mainfrom
claude/vigilant-gauss-7jXfW

Conversation

@bernardladenthin
Copy link
Copy Markdown
Owner

Replaces duplicated CLAUDE.md content with one-line pointers into the
sibling workspace repo, where the canonical text now lives:

  • Javadoc Conventions, SpotBugs Suppressions, jqwik prompt-injection
    policy: pointer per section
  • @VisibleForTesting design-fit / Package hierarchy / Class & method
    naming review TODOs: collapsed into a single workspace pointer
  • "Abstract guidelines to workspace" and "Standardised CLAUDE.md
    template" TODOs: marked DONE

This repo had no per-repo SKILL.md (no .claude/skills/) and no
in-repo writing guides, so only CLAUDE.md changes.

https://claude.ai/code/session_01LzoKmqzgtQsELS5tsH4Wog

claude added 13 commits June 3, 2026 23:54
Replaces duplicated CLAUDE.md content with one-line pointers into the
sibling workspace repo, where the canonical text now lives:

- Javadoc Conventions, SpotBugs Suppressions, jqwik prompt-injection
  policy: pointer per section
- @VisibleForTesting design-fit / Package hierarchy / Class & method
  naming review TODOs: collapsed into a single workspace pointer
- "Abstract guidelines to workspace" and "Standardised CLAUDE.md
  template" TODOs: marked DONE

This repo had no per-repo SKILL.md (no .claude/skills/) and no
in-repo writing guides, so only CLAUDE.md changes.

https://claude.ai/code/session_01LzoKmqzgtQsELS5tsH4Wog
Workspace guides were restructured into a src/+test/ split with
version-suffix file names. java-llama.cpp is Java 8, so only the -8.md
baseline files apply.

https://claude.ai/code/session_01LzoKmqzgtQsELS5tsH4Wog
Patch + minor version bumps verified safe against Maven Central:

- checker-framework 4.1.0 -> 4.2.0 (minor)
- logback-classic 1.5.33 -> 1.5.34 (patch)
- fb-contrib 7.6.4 -> 7.7.4 (minor)
- palantir-java-format 2.66.0 -> 2.91.0 (minor)
- pitest-maven 1.25.1 -> 1.25.3 (patch)

No source changes required.

https://claude.ai/code/session_01LzoKmqzgtQsELS5tsH4Wog
Adds argsPackageIsALeaf to LlamaArchitectureTest: classes in
net.ladenthin.llama.args must not depend on the root API package or
the json parser package. Catches a future drift like an enum gaining
a "convenient" helper that pulls in JNI state or a JSON DTO.

The traditional 3-layer layeredArchitecture() rule (Args -> Json ->
Api) was attempted first and rejected on evidence: json parsers/
serializers genuinely depend on root-package DTOs (Pair, ChatMessage,
ContentPart) AND the root API genuinely depends on json parsers
(LlamaIterator, JsonParameters, LlamaModel, ModelParameters import
from json). json and api are peers in the public API layer, not a
stackable hierarchy. Splitting the DTOs into a dedicated
net.ladenthin.llama.value package would enable real layering, but
breaks published public-API FQNs (net.ladenthin.llama.Pair, etc.)
and is out of scope for an ArchUnit rule.

The argsPackageIsALeaf rule pins the only real layered invariant
that the current package design supports. noPackageCycles already
catches the looser "no cycles between subpackages" property.

Tests: 10 of 10 pass (was 9; +1 for the new leaf rule).
A complete sweep of src/main and src/test for every
net.ladenthin.llama.* system property the library understands, with
each property's default value and concrete consumer attached. The
previous README table covered only 5 of the 9 properties (the 4
test-only nomic + vision properties were undocumented in user-facing
docs) and described the 5 it did cover as "all resolved by
LlamaSystemProperties" - which the scan showed to be incorrect.

README changes:
- Replaces the 5-row "System Properties Reference" table with a 9-row
  table covering every net.ladenthin.llama.* property the library
  understands. Columns added: Default value, Scope (runtime vs test),
  and Consumer (which production class or test class reads the
  property). Test-only properties (nomic.path, vision.model,
  vision.mmproj, vision.image) and the runtime-but-tests-also (test.ngl)
  are now visible to users without having to read CLAUDE.md or grep
  the source.
- "All ... resolved by LlamaSystemProperties" claim corrected: only
  runtime properties go through the registry, and even there two
  inconsistencies exist (see CLAUDE.md TODO).
- Adds the MultimodalIntegrationTest self-skip note so users
  understand a partial vision setup still loads.

CLAUDE.md changes:
- The "Optional models" mini-table now cross-links to the README
  reference instead of being the only place the 4 test-only properties
  are documented. Mini-table kept (model paths + the issue numbers
  it regresses) since that's deeper context than the README needs.
- New TODO entry flags the two source-side inconsistencies the scan
  surfaced:
  (1) LlamaSystemProperties.getLibName() is declared but zero
      production callers reach it - either wire it into LlamaLoader's
      filename-resolution path or delete it as dead code;
  (2) OSInfo.java:390 reads "net.ladenthin.llama.osinfo.architecture"
      directly via the literal string, bypassing the registry's
      getOsinfoArchitecture() getter. Same single-source-of-truth
      smell as the recent BAF Radix.HEX consolidation; routing OSInfo
      through the registry getter is a small follow-up.

Deep-scan inventory (9 properties total):
  Runtime (resolved via LlamaSystemProperties, with caveats above):
    net.ladenthin.llama.lib.path             -> LlamaLoader:92
    net.ladenthin.llama.lib.name             -> (declared, zero callers)
    net.ladenthin.llama.tmpdir               -> LlamaLoader:250
    net.ladenthin.llama.osinfo.architecture  -> OSInfo:390 (literal, bypasses registry)
  Test (declared in TestConstants):
    net.ladenthin.llama.test.ngl             -> 8 test classes via Integer.getInteger
    net.ladenthin.llama.nomic.path           -> LlamaEmbeddingsTest
    net.ladenthin.llama.vision.model         -> MultimodalIntegrationTest
    net.ladenthin.llama.vision.mmproj        -> MultimodalIntegrationTest
    net.ladenthin.llama.vision.image         -> MultimodalIntegrationTest

No source / test changes; doc-only commit.
…hitecture()

OSInfo.getArchName() read the override system property via a hard-coded
string literal:

  String override = System.getProperty("net.ladenthin.llama.osinfo.architecture");

bypassing LlamaSystemProperties.getOsinfoArchitecture(), the registry-side
getter built specifically to be the single source of truth for that
property name. The deep scan that produced the README System Properties
Reference (e36f631) surfaced this as one of two registry-bypass smells;
fixing it here closes the first.

Routing through the registry keeps the property name in exactly one
place, mirrors the recent BAF Radix.HEX consolidation (where every
literal '16' radix was replaced by a single Radix.HEX constant), and
means future renames or scope tightenings of the property only have to
land in LlamaSystemProperties.

Tests: OSInfoTest 16/16 pass (the override-set branch is already
covered by the test that sets ARCH_OVERRIDE_PROP); mvn compile clean.
…tLibName()

Deep forensic comparison against kherud/java-llama.cpp (the upstream)
showed that net.ladenthin.llama.lib.name has been a documentation-only
ghost since 2023:

Upstream history of "lib.name":
  c8f57f7 (initial JNI commit) - Javadoc mention AND real code consumer:
    String nativeLibName = System.getProperty("de.kherud.llama.lib.name");
    if (nativeLibName == null) { ... }
  7a852d1 - README documents it (-Dde.kherud.llama.lib.name=myname.so)
  6bb63e1 ("add ggml shared library to binding") - CODE CONSUMER DELETED.
    The loader was extended to load multiple shared libraries (ggml and
    jllama as separate files); the single-name-override model became
    incompatible. Javadoc + README mentions were left behind.
  6d0c4af - README mention removed.

  The Javadoc lie in LlamaLoader.java survives upstream to this day.

Bernard's fork inherited the Javadoc lie verbatim and later added a
LlamaSystemProperties.getLibName() getter for "completeness" (every
documented property gets a registry getter) - but the loader code still
hardcodes "jllama" and ignores the override.

Why delete rather than re-wire:
- Upstream deliberately removed the consumer to support multi-library
  loading. Re-wiring would either reintroduce the same single-name
  limitation upstream removed, or need a new design covering both
  libraries.
- This fork has added even more dimensions (CPU / CUDA /
  OpenCL-Adreno classifiers); the "one override filename" idea is
  even less applicable now than in upstream's day.
- No caller has ever asked for it; the README will be more honest
  with one fewer documented-but-broken row.

Changes:
- LlamaSystemProperties.getLibName() deleted (~5 lines).
- LlamaLoader.java Javadoc fixed: lib.name removed from the "set these
  properties" sentence; replaced with a multi-paragraph history note
  citing upstream commit 6bb63e1 so future readers understand the
  property's lifecycle and don't try to "re-add" it.
- README System Properties Reference: lib.name row dropped (was 9 rows,
  now 8).
- CLAUDE.md: the registry-cleanup TODO is closed for both subitems
  (lib.name DELETED here; osinfo.architecture FIXED in 3ae6c81 by
  routing OSInfo.getArchName() through the registry getter).

Tests: OSInfoTest 16/16 + LlamaLoaderTest 21/21 = 37/37 pass.
mvn compile clean.
Emits a single info-level summary line at the end of every non-streaming
generation (complete / chat), mirroring what the llama.cpp CLI prints:

  prompt: 12 tok in 84.3 ms (142.4 tok/s) | gen: 256 tok in 5031.7 ms (50.9 tok/s) | cache: 0

Speculative-decoding runs append:
  | draft: 50 (35 accepted)

Implementation:
- New TimingsLogger utility class with two public methods:
    format(Timings) -> single-line String (exposed so CLI sinks can reuse)
    log(Timings)    -> emits format(...) at INFO on
                       net.ladenthin.llama.timings (dedicated logger so
                       users can suppress it via logback without touching
                       the rest of net.ladenthin.llama).
- log() is a no-op for null and for all-zero Timings (typical on parse
  failure / early cancellation). No noise from non-event paths.
- Wired into both result parsers right after the Timings instance is
  built:
    json/CompletionResponseParser#parseCompletionResult
    json/ChatResponseParser#parseResponse
- Tests: 7 unit tests in TimingsLoggerTest pin the format byte-exact for
  the standard case, draft segment presence/absence, cache-hit
  rendering, dedicated-logger SLF4J pipeline delivery, all-zero
  no-op, and null no-op. Uses LogCaptor (the same harness LoggingSmokeTest
  uses for OSInfo).

Streaming generation (LlamaIterable / LlamaIterator) is not yet hooked.
The streaming iterator does not surface a clean "I am done" callback
visible from the public API today; threading that through is a separate
follow-up. Non-streaming covers the most common code path and gives
users an immediate signal.

The remaining first-batch items from the feature-investigation backlog
in CLAUDE.md are now the UTF-8 boundary-safe streaming decoder and a
jbang single-file example.

Tests run (per the test-execution policy — no full surefire):
- mvn compile / mvn test-compile: clean.
- mvn test -Dtest='TimingsLoggerTest': 7/7 pass.
- mvn test -Dtest='CompletionResponseParserTest,ChatResponseParserTest,
  LoggingSmokeTest,LlamaArchitectureTest': 58/58 pass (covers both
  parser wire-in sites plus the architecture invariants).
Move CLAUDE.md "Open TODOs" content into a dedicated TODO.md so
CLAUDE.md keeps its role as orientation / build-commands /
architecture, and TODO.md becomes the single per-repo home for open
work and DONE history.

TODO.md splits open work into:
- jllama-specific: upstream feature exposure (skip_download is
  DONE, --spec-draft-backend-sampling / reasoning control /
  max_outputs / MtP / suppress_tokens are queued by policy), the
  similar-projects feature backlog, the GraalVM Native Image
  evaluation
- cross-cutting slice: jqwik pin, @VisibleForTesting audit,
  null-safety follow-up, SpotBugs effort=Max, additional ArchUnit
  rules
- Done history: the full strictness-ladder + workspace-migration
  trail (Werror, Checker, module-info @NullMarked, LogCaptor,
  setSkipDownload, LlamaSystemProperties cleanup, etc.)

CLAUDE.md "Open TODOs" section now carries only a pointer to
TODO.md and to workspace/crossrepostatus.md.

Cross-repo coordination: each of the 4 sibling repos does the same
extraction in this session.

https://claude.ai/code/session_01LzoKmqzgtQsELS5tsH4Wog
Cross-repo decision to use Lombok at the latest version for
compile-time generation of equals/hashCode/toString without runtime
reflection. See BAF commit 31eb6ef for the full decision record.

`<scope>provided</scope>` keeps Lombok off the runtime classpath;
`requires static lombok;` in module-info.java keeps it compile-time
only. The new lombok.config pins the standard cross-repo settings
(addLombokGeneratedAnnotation, callSuper = call, stopBubbling).

This commit is dependency-only. Per-class @tostring /
@EqualsAndHashCode adoption is deferred until after the BAF
restoration loop concludes and the user reviews the BAF approach.

https://claude.ai/code/session_01LzoKmqzgtQsELS5tsH4Wog
Two badge-row changes in the Build section:

1. Add Lombok 1.18.46 badge between Maven Enforcer and jqwik —
   mirroring BAF (c7bfbc4) and plugin (38a8fdb) so all three
   repos that use Lombok carry the badge at the same slot.

2. Promote the [llama.cpp #b9495] badge from its previous slot
   (after JMH, near the end of the Build row) to row 3, right
   under Java + Platform. The pinned upstream version is the
   single most defining attribute of a JNI binding to llama.cpp,
   so a reader scanning the top of README needs to see it
   immediately — not buried after the testing/benchmarking tool
   badges. New ordering: Java -> Platform -> llama.cpp #b9495 ->
   JPMS -> JUnit -> JSpecify ...

streambuffer keeps its hand-written toString and does NOT adopt
Lombok, so the badge does NOT apply there.

https://claude.ai/code/session_01LzoKmqzgtQsELS5tsH4Wog
…n sources

Apply the BAF/plugin Lombok migration pattern to java-llama.cpp. Value-shaped
types get @tostring + @EqualsAndHashCode; mutable lifecycle handles and
operational classes get @tostring only (identity is the correct equals
semantics); classes with semantically-meaningful handwritten toString
implementations keep their custom toString and gain @EqualsAndHashCode only.

Build wiring fixes (mirrors BAF/plugin):
- pom.xml: add Lombok to <annotationProcessorPaths> AND extend the explicit
  -processor argument list to include both Lombok SPI processors
  (AnnotationProcessorHider$AnnotationProcessor and $ClaimingProcessor)
  alongside the existing Checker Framework NullnessChecker.
- lombok.config: switch callSuper default from "call" to "skip" since most
  value classes extend Object directly.

Value types (Lombok @tostring + @EqualsAndHashCode):
- Pair, ChatChoice, ChatResponse, ContentPart, ToolDefinition

Value types with derived/aggregated toString fields:
- Usage (totalTokens via @ToString.Include on the getter)
- TokenLogprob (topLogprobs rendered as size via @ToString.Include)
- Timings (full field dump)

Value types with semantically-meaningful handwritten toString preserved
(@EqualsAndHashCode only, toString documented as intentional):
- ChatMessage ("role: content" conversation-trace format)
- ToolCall ("name(args)[id]" function-call syntax)
- CompletionResult, LlamaOutput (return generated text verbatim — public
  API contract)
- ModelMeta, ServerMetrics (re-serialise to compact JSON for assertEquals)
- JsonParameters (emits actual JSON the native server consumes)
- CliParameters (emits CLI argv-style string for the native binary)

Subclasses of JsonParameters/CliParameters get @EqualsAndHashCode(callSuper=true):
- InferenceParameters, ModelParameters

Operational / lifecycle classes (Lombok @tostring only; identity-equals is
correct; native handles and lambdas excluded from rendered output):
- LlamaModel (parser collaborators rendered, native ctx handle visible)
- LlamaIterator, LlamaIterable, LlamaPublisher (owning model excluded —
  recursive native-state dump)
- Session (model + lambdas + lock excluded)
- CancellationToken, ChatRequest (lambdas excluded)
- LlamaLoader, LlamaSystemProperties, NativeLibraryPermissionSetter,
  ProcessRunner, Java8CompatibilityHelper

Test update:
- PairTest.testHashCodeMatchesObjectsHash renamed and rewritten to
  verify the hashCode contract (non-zero, varies by field) rather than
  pinning to Objects.hash. The PIT-mutation-killing intent is preserved
  but no longer assumes the specific implementation.

Intentionally skipped (rationale documented inline):
- OSInfo — vendored from sqlite-jdbc with explicit "only deviations" policy;
  adding Lombok would diverge further. Spotless line-wrapping is incidental.
- LlamaException, ModelUnavailableException — extend Throwable, which
  already provides toString and identity-equals.
- StopReason, LogLevel — enums, inherit toString from Enum.
- LoadProgressCallback, ToolHandler — interfaces.
- SkipDownloadFailureTranslator, TimingsLogger — non-instantiable utility
  classes (private constructor + all-static methods).

All 888 runnable tests pass; the single RerankingModelTest error is a
pre-existing UnsatisfiedLinkError on this sandbox (no native library
built in restricted-network env, per CLAUDE.md).

https://claude.ai/code/session_01LzoKmqzgtQsELS5tsH4Wog
Lombok-generated equals / hashCode / canEqual / toString carry the
textbook polynomial-hash pattern (int result = 1; result = result * 59
+ ...; return result;) which fb-contrib's USBR detector reads at the
bytecode level as a store-then-immediate-return.

SpotBugs core already skips its own detectors on members carrying
@lombok.Generated (emitted by lombok.config's
lombok.addLombokGeneratedAnnotation = true), but fb-contrib runs as a
separate plugin family and does not honour that annotation. A method-
name-based <Match> covers every member Lombok can emit.

Clears 18 jllama findings at SpotBugs Max+Low. The collateral cost is
small: any handwritten equals/hashCode/toString that genuinely
stores-then-immediately-returns is either a debugger-friendly local-
variable pattern or a micro-optimisation, both intentional here.

https://claude.ai/code/session_01LzoKmqzgtQsELS5tsH4Wog
* forward to the native CLI. Replacing it with a Lombok field dump would break that
* consumer contract.
*/
@EqualsAndHashCode
Comment thread src/main/java/net/ladenthin/llama/JsonParameters.java Fixed
* parameters as a JSON object string consumed by the native server.</p>
*/
@SuppressWarnings("unused")
@EqualsAndHashCode(callSuper = true)
* {@code equals}/{@code hashCode} are intentionally NOT generated: iterators are
* lifecycle handles tied to a single in-progress task, managed by identity.</p>
*/
@ToString
* {@code equals}/{@code hashCode} are intentionally NOT generated: model instances own
* a native context and are managed by reference identity, not by value.</p>
*/
@ToString
* string consumed by the native binary.</p>
*/
@SuppressWarnings("unused")
@EqualsAndHashCode(callSuper = true)
Defers the 25-site scope-tightening pass until after the planned
package-architecture refactor. Same rationale and decision recorded in
workspace/crossrepostatus.md alongside the parallel BAF suppression.

Why suppress instead of fix now:
- Current jllama package layout groups production code in
  net.ladenthin.llama + a thin sibling package set. Any method called
  only by same-package callers is flagged as "could be package-private".
  Correct today, false tomorrow once the refactor splits the root
  package into proper layers — cross-layer calls then need public.
- Tightening every site now creates mechanical churn the refactor
  would revert.
- TODO marker added to workspace/crossrepostatus.md under the
  "Affects BAF + jllama (multi-package repos)" section so the rule
  can be re-enabled deliberately the week the layers stabilise.

Cross-repo alignment: same suppression added to BAF
(BitcoinAddressFinder/spotbugs-exclude.xml, 33 sites there).
…x+Low row

OPM_OVERLY_PERMISSIVE_METHOD is now project-wide suppressed (07109cc)
pending the package-architecture refactor. The TODO row now describes
the remaining ~65 findings (DRE 20, WEM 14 + low-count residue) plus
adds the lifecycle TODO to re-enable the rule when the layered
package structure stabilises — same shape as BAF's matching entry.
…l signatures

LlamaException extends RuntimeException (LlamaException.java:18), so the
20 'throws LlamaException' declarations on LlamaModel are unchecked
exceptions in throws clauses — the canonical DRE_DECLARED_RUNTIME_EXCEPTION
fb-contrib pattern. JDK convention is to advertise unchecked exceptions
via Javadoc @throws only, never the throws clause.

Sites (all in LlamaModel.java):
- 4 Java methods: completeAsJson (2 overloads), getMetricsTyped, getModelMeta
- 16 native methods: requestCompletion, receiveCompletionJson, loadModel,
  loadModelWithProgress, handleRerank, handleCompletions,
  handleCompletionsOai, handleInfill, handleEmbeddings, handleTokenize,
  handleDetokenize, getModelMetaJson, configureParallelInference,
  handleSlotAction, handleChatCompletions, requestChatCompletion

The existing '@throws LlamaException' Javadoc tags were preserved on
every site (verified at lines 65, 69, 90, 525, 540, 644, 737, 755,
773, 793) so javadoc:jar still documents the unchecked exception
contract for callers.

Compatibility audit:
- Public API: callers either catch LlamaException explicitly (works
  without the throws declaration), declare it in their own throws
  (overrides can throw fewer or unchecked), or don't catch (still
  propagates unchecked). Zero caller-side break.
- Native bridge: JNI throws via ThrowNew regardless of the Java throws
  clause — no JNI-side enforcement of the clause.
- Tests: 888 of 889 tests pass; the 1 error is an unrelated
  UnsatisfiedLinkError because this sandbox has no built native
  library (documented in CLAUDE.md as a known mvn-test-without-cmake
  failure mode), not caused by this change.
- mvn javadoc:jar -DskipTests=true -Dgpg.skip=true: BUILD SUCCESS.

SpotBugs Max+Low: DRE_DECLARED_RUNTIME_EXCEPTION goes 20 → 0.
Total jllama findings: 65 → 45. Top remaining: WEM 14, UVA 5,
MDM_WAIT_WITHOUT_TIMEOUT 4, THROWS_METHOD_THROWS_RUNTIMEEXCEPTION 4.
…leared)

Switches 4 ModelParameters setter validations to typed
IllegalArgumentException with in-scope state, clearing 4 WEM and 2
THROWS_METHOD_THROWS_RUNTIMEEXCEPTION findings in one pass (the two
THROWS_METHOD sites overlapped with WEM on setRepeatLastN and
setDryPenaltyLastN, which previously threw bare RuntimeException).

Sites:
- setPriority           : message now includes value + allowed range
                          0=normal 1=medium 2=high 3=realtime
- setPriorityBatch      : same shape
- setRepeatLastN        : bare RuntimeException -> IllegalArgumentException;
                          message includes value + "must be >= -1; -1 =
                          ctx_size, 0 = disabled"
- setDryPenaltyLastN    : bare RuntimeException -> IllegalArgumentException;
                          message includes value + "must be >= -1; -1 =
                          context size, 0 = disabled"

Test consistency cleanup (ModelParametersTest):
- Lines 101, 122 tightened from assertThrows(RuntimeException.class,
  ...) to assertThrows(IllegalArgumentException.class, ...) to match the
  4 sibling tests at 49, 54, 69, 74 that already assert the specific
  type. The narrower assertion is strict-compatible: the prior assertion
  would also pass since IllegalArgumentException extends RuntimeException.

Compatibility:
- ModelParameters is final (line 21) — no subclass override risk.
- Callers catching RuntimeException continue to catch the new
  IllegalArgumentException unchanged.
- Builder chain return-type unchanged.

Test slice green: 288 tests across ModelParametersTest (62),
ModelParametersExtendedTest (140), InferenceParametersTest (86),
ChatAdvancedTest.

SpotBugs Max+Low: WEM goes 14 -> 10, THROWS_METHOD_THROWS_RUNTIMEEXCEPTION
goes 4 -> 2. Total jllama: 45 -> 39.
…(5 cleared)

Adds in-scope state (slotId + transcript size) to the 5 state-machine
IllegalStateException messages thrown by Session.send / stream /
commitStreamedReply / save / restore.

Before:
    throw new IllegalStateException(
        "stream in progress; call commitStreamedReply(...) before send(...)");

After:
    throw new IllegalStateException(
        "stream in progress on slot " + slotId
            + " (transcript=" + turns.size() + " turns)"
            + "; call commitStreamedReply(...) before send(...)");

Why both fields:
- slotId identifies which session got stuck in a multi-session setup
  (a process can hold many Sessions on the same model, one per slot).
- turns.size() shows how far the transcript progressed before the
  state-machine violation — useful when triaging "the session went
  weird around message N" reports.

Compatibility:
- Session is final (line 41) — no subclass override risk.
- No test asserts on the exception message text (verified across
  SessionConcurrencyTest, LlamaModelTest).

THROWS_METHOD_THROWS_RUNTIMEEXCEPTION findings on Session.send and
Session.stream (the catch+cleanup+rethrow pattern at line 112 / 138)
are deliberately NOT addressed in this batch; they are deferred to
their own investigation alongside the existing BAF suppression
(spotbugs/spotbugs#3918 + PR #4087 lifecycle).

SpotBugs Max+Low: WEM goes 10 -> 5. Total jllama: 39 -> 34.
… (10 cleared)

Batch 4 — leaf-class WEM (5 sites):
  - ChatMessage.requireNonEmpty: message now distinguishes
    null vs size=0 input
  - ChatRequest.setMaxToolRounds: message now includes the
    rejected value
  - ContentPart.imageBytes: message now includes bytes.length so the
    operator knows which call failed
  - LlamaLoader.getNativeResourcePath: message now includes the active
    classLoader so the operator can debug class-loading issues
  - LlamaPublisher.subscribe: message now references reactive-streams
    §1.9 and includes the calling thread name (the spec rule citation
    helps callers, the thread name is the WEM runtime expression)

Batch 5 — UVA array → varargs (5 sites):
  - InferenceParameters.setPenaltyPrompt(int[]) -> int...
  - LlamaModel.decode(int[]) -> int...
  - LlamaModel.decodeBytes(int[]) -> int... (native)
  - LlamaModel.handleDetokenize(int[]) -> int... (native)
  - ParameterJsonSerializer.buildIntArray(int[]) -> int...

Varargs is source-compatible with all existing array call sites; the
new call shape f(1, 2, 3) is additionally allowed. Bytecode signature
stays [I with the ACC_VARARGS bit flipped on — JNI doesn't enforce
the bit, so native method signatures are unchanged on the C side.

Test impact (LlamaPublisherTest.nullSubscriberThrows):
- Previously: assertEquals("subscriber", ...) — exact-match assertion.
- Now: assertTrue(msg.startsWith("reactive-streams §1.9: subscriber
  must not be null"), ...) — prefix-match so the runtime thread-name
  suffix doesn't break the assertion across environments.

Test slice green: 162 tests across ChatMessageTest (2),
ChatRequestTest, ContentPartTest (14), LlamaLoaderTest (21),
LlamaPublisherTest (4), InferenceParametersTest (86),
ParameterJsonSerializerTest (35).

SpotBugs Max+Low: WEM goes 5 -> 0, UVA goes 5 -> 0.
Total jllama: 34 -> 25.
…dapters

Removes the hand-rolled reactive-streams Publisher and the associated
mandatory runtime dependency on org.reactivestreams. Adds consumer-facing
documentation showing how to wrap LlamaIterable with each mainstream
reactive library's resource-management primitive — verified end-to-end
by a new ReactorIntegrationTest using test-scope reactor-core.

Why now
=======

LlamaPublisher (introduced in PR #188 as section 2.3 of the Kotlin SDK
feature comparison) had zero non-test callers. The feature-investigation
document itself describes its source spec as "no longer a roadmap". The
real-world Android consumer LLaMAndroid uses the existing LlamaIterable
API directly inside a Kotlin flow { } block — bypassing the publisher
entirely. Upstream kherud/java-llama.cpp never carried this class.

LlamaIterable already implements Iterable<LlamaOutput> + AutoCloseable —
the contract every reactive library needs to bridge a blocking source:

  - Project Reactor : Flux.using(supplier, Flux::fromIterable, ::close)
  - RxJava 3 / RxAndroid : Flowable.using(supplier, Flowable::fromIterable, ::close)
  - Kotlin Flow : flow { iterable.use { for (x in it) emit(x) } }
  - Akka Streams : Source.fromIterator(() -> iterable.iterator())

These are the canonical patterns the libraries themselves recommend for
blocking sources. Keeping a Publisher in the binding forced every
consumer onto the org.reactivestreams runtime dep just to access a class
nobody called.

Critical correctness note: Flux.fromIterable / Flowable.fromIterable do
NOT auto-close AutoCloseable iterables on cancel — the consumer must use
.using(...) or equivalent. The README documents this caveat explicitly;
the ReactorIntegrationTest pins the correct pattern.

Changes
=======

Deletes:
  - src/main/java/net/ladenthin/llama/LlamaPublisher.java (175 LOC)
  - src/test/java/net/ladenthin/llama/LlamaPublisherTest.java (204 LOC)
  - LlamaModel.streamPublisher / streamChatPublisher (23 LOC)
  - pom.xml org.reactivestreams runtime dep + version property
  - module-info.java javadoc reference

Adds:
  - src/test/java/net/ladenthin/llama/ReactorIntegrationTest.java
    Mock-iterable contract test (always runs) + real-model gate test
    proving end-to-end cancel propagation via Flux.using + LlamaIterable.close
  - pom.xml reactor-core + reactor-test at test scope, 3.6.11
  - README.md new "Reactive integration" section covering Reactor,
    RxJava 3, Kotlin Flow (with LLaMAndroid reference), Akka Streams,
    and the why-no-built-in-Publisher rationale

Updates:
  - docs/feature-investigation-llama-stack-client-kotlin.md: section 2.3
    status now reads "SHIPPED + REVERTED REACTIVE PUBLISHER" with full
    rationale and pointer to the README
  - TODO.md: new Done entry capturing the decision trail

Net: -331 LOC (-503 source, +172 test/docs); -1 runtime dep
(org.reactivestreams); +2 test-scope deps (reactor-core, reactor-test).
SpotBugs Max+Low: total drops 25 -> 19 (all 6 LlamaPublisher$LlamaSubscription
findings cleared as a side effect: MDM_WAIT_WITHOUT_TIMEOUT x4 +
CWO_CLOSED_WITHOUT_OPENED + PRMC_POSSIBLY_REDUNDANT_METHOD_CALLS).

Tests
=====

ReactorIntegrationTest: 2 tests, mock variant always runs, real-model
variant gated on TestConstants.MODEL_PATH. Mock test proves Reactor
backpressure (request(2) delivers exactly 2 items, never more) and
cleanup-on-cancel (Flux.using cleanup function fires on cancel).
887 of 888 tests pass (the 1 error is the known sandbox-without-native-lib
UnsatisfiedLinkError in RerankingModelTest, unrelated to this change).
NullAway and the package's @NullMarked declaration prove the three
getters (ChatMessage.getContent, ToolCall.getArgumentsJson,
ToolDefinition.getDescription) are @nonnull at compile time, so the
runtime "value == null ? "" : value" ternaries and the
"if (getDescription() != null)" guard at three sites in
buildMessagesJson / buildToolsJson are dead.

Also narrows the catch in buildToolsJson from Exception to IOException.
The only checked exception MAPPER.readTree(String) can throw is
JsonProcessingException, which extends IOException — narrowing is
honest and removes the catch-Exception code smell.

Adds: import java.io.IOException.

Compatibility:
- Empty-string output on a null content/arguments is impossible per
  NullAway, so removing the fallback never changes runtime behaviour.
- The 7 tests in ChatResponseTest (including
  buildMessagesJsonRoundTripsToolTurns and
  buildToolsJsonInlinesParameterSchema, which exercise both modified
  code paths) stay green.

SpotBugs Max+Low:
  - RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE: 4 -> 1
    (ChatMessage.requireNonEmpty:103 remains, separate cluster)
  - REC_CATCH_EXCEPTION: 1 -> 0
Total jllama: 19 -> 15.
… cleared)

The parts parameter is typed List<ContentPart> (no @nullable); the
package is @NullMarked so NullAway proves the value cannot be null at
the check site. The 'parts == null ||' branch and the ternary
'(parts == null ? "null" : "size=0")' branch are dead code.

After removal:
  - Single guard: if (parts.isEmpty())
  - Message keeps a runtime expression (parts.size()) so the WEM
    requirement satisfied in Batch 4 stays satisfied.

Compatibility:
  - requireNonEmpty is private static — only called from ChatMessage
    constructors in the same @NullMarked package, so NullAway's proof
    covers every caller statically.
  - No test asserts on the message text.

SpotBugs Max+Low: RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE 1 -> 0.
Total jllama: 15 -> 14.
…tics

Removes the single use of Checker Framework's @polynull qualifier in
production code (JsonParameters.toJsonString). Under @NullMarked, the
elegant @polynull contract (null in → null out, non-null in → non-null
out) is not expressible in plain JSpecify, so the prior shape required
@SuppressWarnings("NullAway") and forced fb-contrib to flag
AI_ANNOTATION_ISSUES_NEEDS_NULLABLE (which it does not understand
@polynull).

The cleanup splits the helper and routes the 7 user-facing setters
through a new null-aware put method:

  toJsonString(String text)
    Serialize a NON-NULL text to JSON form. Used directly by the two
    enum-driven setters (setReasoningFormat, setContinueFinalMessage)
    where the input is always non-null.

  putOptionalJson(String key, @nullable String text)
    Conditionally store: when text is null the call is a no-op,
    otherwise the value is JSON-encoded and inserted. Used by the
    seven user-facing setters (setPrompt, setInputPrefix,
    setInputSuffix, setGrammar, setPenaltyPrompt, setChatTemplate,
    setToolChoice).

Behaviour: setX(null) now omits the key from the parameters map
instead of inserting a null value entry. On the native side
(nlohmann::json) both a missing key and a null-valued key signal
"use the default", so the observable behaviour is preserved. The
single test that exercises this (InferenceParametersTest
.testToJsonStringNull) continues to pass — its assertion
(parameters.get("grammar") returns null) is satisfied identically
by absence as it was by a null-value entry.

Also removes the now-unused org.checkerframework.checker.nullness.qual
.PolyNull import, leaving the production-code Checker Framework
footprint at exactly 2 lines, both deliberate
@SuppressWarnings("method.invocation") on LlamaModel constructors
that call native loadModel — required because Checker's
@UnderInitialization analysis cannot see that the native callee
does not dereference this.

SpotBugs Max+Low: AI_ANNOTATION_ISSUES_NEEDS_NULLABLE 1 -> 0.
Total jllama: 14 -> 13.

All 93 tests across InferenceParametersTest (86) and ChatResponseTest
(7) green; the modified setters and the JSON build path are both
covered.
…ce to Optional<String>

Brings the codebase to 100% on the established "nullable getters return
Optional<T>" convention (CLAUDE.md / TODO.md note: "Public-API methods
that may legitimately have no value use Optional<T> rather than
@nullable T").

Migrated:
  - ChatMessage.getToolCallId() : @nullable String -> Optional<String>
  - ChatRequest.getToolChoice() : @nullable String -> Optional<String>

Updated callers (using the project's existing .ifPresent idiom):

  ChatRequest.buildMessagesJson (was):
      final String toolCallId = m.getToolCallId();
      if (toolCallId != null) {
          obj.put("tool_call_id", toolCallId);
      }
  After:
      m.getToolCallId().ifPresent(id -> obj.put("tool_call_id", id));

  LlamaModel.requestChatCompletion (was):
      final String toolChoice = request.getToolChoice();
      if (toolChoice != null) {
          params.setToolChoice(toolChoice);
      }
  After:
      request.getToolChoice().ifPresent(params::setToolChoice);

StopReason.getStopType() was DELIBERATELY NOT migrated. It is paired
with StopReason.fromStopType(@nullable String) in a round-trip test:

  assertSame(reason, StopReason.fromStopType(reason.getStopType()));

Switching the getter to Optional<String> would force the test caller
to do reason.getStopType().orElse(null), which defeats the purpose.
The factory takes @nullable per the project convention (Optional as
parameter is anti-idiom per Effective Java §55), and the symmetric
getter follows.

Remaining @nullable T return types in production:
  - JsonParameters.toJsonString : private helper, kept as @nullable
    (call sites use the new putOptionalJson method instead)
  - StopReason.getStopType : paired with @nullable factory
  - ParameterJsonSerializer.buildMessages : internal Jackson builder
  - Timings.fromJson : factory, takes @nullable JsonNode

Tests: 95 green across ChatMessageTest, ChatResponseTest,
ChatAdvancedTest, InferenceParametersTest. No test asserted on
@nullable String return shape of either migrated getter.
… Session

Eliminates the catch-cleanup-rethrow pattern in Session.send and
Session.stream by moving transcript management into a new ChatTranscript
class whose API surface enforces the two-phase commit invariant by
construction.

The catch-rethrow pattern fb-contrib flagged
(THROWS_METHOD_THROWS_RUNTIMEEXCEPTION) was the symptom of
"mutate shared state, then if the call fails, undo the mutation". The
ChatTranscript design eliminates the root cause: there is no API to
commit half a round. The only round-commit method, appendRound(user,
assistant), appends both turns atomically; the wire format sent to the
model is built via messagesWithPendingUserTurn(...) which returns a
fresh list WITHOUT mutating the transcript. On model failure, the
caller never reaches appendRound — no rollback logic is required.

Side benefit — testable as running documentation. Extracting the
transcript management decouples the invariant from LlamaModel (whose
static initializer loads the native library and is unmockable in
test environments without the native library). The new
ChatTranscriptTest exercises the invariant with 12 tests across two
categories:

  @nested "mechanical API behaviour" (8 tests):
    - appendRound commits both turns atomically
    - appendUserTurn + appendAssistantTurn match appendRound
    - messagesWithPendingUserTurn does NOT mutate the transcript
    - messagesWithPendingUserTurn returns a fresh list each call
    - snapshot includes the system message when configured
    - snapshot omits the system message when null or empty
    - snapshot is unmodifiable
    - getSystemMessage returns null when absent

  @nested "two-phase commit pattern - running documentation" (4 tests):
    - fresh transcript untouched when model throws
    - existing transcript byte-for-byte unchanged when model throws
    - success commits user + assistant atomically
    - stream() shape - user turn only, assistant follows via
      commitStreamedReply

Each two-phase test composes the ChatTranscript API the same way
Session.send and Session.stream do, so reading the test doubles as
documentation of the design contract.

Net:
  - New file: src/main/java/net/ladenthin/llama/ChatTranscript.java
  - New file: src/test/java/net/ladenthin/llama/ChatTranscriptTest.java
  - Session.java: 247 lines -> 247 lines (no net change; delegates to
    ChatTranscript). The catch-rethrow blocks are gone; the
    buildParamsWithPendingUserTurn helper now delegates to
    ChatTranscript.messagesWithPendingUserTurn.

SpotBugs Max+Low:
  - THROWS_METHOD_THROWS_RUNTIMEEXCEPTION goes 2 -> 0 by design, not
    by suppression. Cross-repo lifecycle TODO for BAF (PR #4087) can
    take inspiration from this refactor on AbstractProducer.produceKeys
    if/when that two-phase commit fits.
  - IMC_IMMATURE_CLASS_NO_EQUALS goes 2 -> 3 (one new finding on the
    new ChatTranscript class — identity-by-design like the existing
    CancellationToken and ChatRequest sites; will fold into the
    existing class-level suppression block).
Total jllama: 13 -> 12.

Test slice green: 117 tests across ChatTranscriptTest (12),
ChatMessageTest (2), ChatResponseTest (7), InferenceParametersTest
(86), LlamaArchitectureTest (10).
Convert ChatRequest from a mutable fluent builder into a fully immutable
value class with a functional "wither / appender" API:

  ChatRequest.empty()
      .appendMessage("user", "hi")
      .withMaxToolRounds(2)
      .withInferenceCustomizer(p -> p.setSeed(1));

Each modification routes through a single private all-args constructor
with one field replaced, allocating a new ChatRequest. The original is
never touched, so a caller can safely hold an intermediate request and
derive variants without hidden state changes.

Notable side effect: this fixes a hidden mutation bug in
LlamaModel.chatWithTools(). The previous agent loop mutated the caller's
ChatRequest across rounds (adding the assistant turn + each tool result
to the same builder). The loop is now rebound to a local `current` that
is replaced on every append, preserving the caller's request.

Tests:
- New ChatRequestTest documents the immutability + value-equality
  contract in 18 cases across four @nested groups (immutability,
  equality, validation, JSON-build read-only).
- LlamaModelTest and ChatResponseTest call sites migrated from
  `new ChatRequest()...addMessage(...)` to
  `ChatRequest.empty()...appendMessage(...)`.

SpotBugs:
- IMC_NO_EQUALS goal cleared at source (the class is now a true value
  object with Lombok @EqualsAndHashCode by value).
- EI_EXPOSE_REP on getMessages()/getTools() suppressed with explicit
  rationale: the fields ARE Collections.unmodifiableList views; the
  test suite verifies that mutation attempts throw
  UnsupportedOperationException, so the finding is a false positive.
Commit f97c85d (spotbugs RCN cleanup) removed the null branch inside
requireNonEmpty under the assumption that the JSpecify @nonnull default
made parts==null unreachable. The deliberately type-cast call site in
MultimodalMessagesTest.nullPartsListIsRejected bypasses that static
check at compile time, so the runtime path now NPEs inside
requireNonEmpty(parts).isEmpty() instead of throwing the documented
IllegalArgumentException.

Restore the contract by validating null at the public-constructor
boundary (the right place for external-input validation) and keep the
private requireNonEmpty focused on the empty case.
Convert InferenceParameters from a mutable fluent builder into a fully
immutable value class with a functional wither API:

  InferenceParameters params = InferenceParameters.of("two plus two?")
          .withNPredict(8)
          .withSeed(1)
          .withTemperature(0.2f);

The parent JsonParameters base is reshaped to match: parameters map is
final and Collections.unmodifiableMap-wrapped, and the helpers
putScalar / putEnum / putOptionalJson are replaced by withScalar /
withEnum / withOptionalJson / withRaw, all of which allocate a new
subclass instance through the abstract withParameters(Map) factory
hook. ModelParameters extends a different parent (CliParameters) and
is unchanged — it is constructed once and consumed once, so the
immutability refactor brings no correctness payoff there but ~250 test
line changes; deliberately skipped to keep this commit focused.

Hidden mutation bug fixed: LlamaModel.complete /
completeWithStats / chatComplete and the cancellable complete-with-token
variant silently called parameters.setStream(true|false) on the
caller's instance. They now bind a local derivation via withStream so
the caller's parameters object is never touched. Same pattern applied
to LlamaIterator.

API breaks (intentional, alongside the parameter rename):
- Consumer<InferenceParameters> -> UnaryOperator<InferenceParameters>
  in Session and ChatRequest. The customiser must return its
  transformed result because the input is immutable; lambdas like
  `p -> p.withSeed(1).withNPredict(8)` keep working with the
  expression-form return.
- ChatRequest.applyCustomizer now returns InferenceParameters instead
  of being a void mutator; callers (only LlamaModel.chat) updated.

Tests:
- JsonParametersTest rewritten to cover the new wither helpers
  (withScalar / withEnum / withRaw / withOptionalJson) plus the
  unmodifiable-map invariant. The legacy CliParameters putScalar /
  putEnum tests are preserved unchanged because ModelParameters still
  uses them.
- Bulk-renamed setX -> withX across the entire test surface
  (InferenceParametersTest 71+, ChatAdvancedTest 84, LlamaModelTest 90,
  ChatScenarioTest 64, MemoryManagementTest 40, plus smaller files and
  all examples), preserving ModelParameters' fluent setX chains where
  the overlap methods (setSeed, setGrammar, setJsonSchema, setSamplers,
  setChatTemplate, setChatTemplateKwargs, setReasoningFormat) appear.
- A handful of tests that did `params.setX(...)` without capturing the
  return value were rewritten to `params = params.withX(...)`.

SpotBugs Max+Low: net unchanged at 6 findings. The one new
OCP_OVERLY_CONCRETE_PARAMETER on InferenceParameters.withReasoningFormat
is suppressed with the same design-intent rationale as the existing
ModelParameters OCP block (the narrow enum type is the API contract;
widening to CliArg would silently accept any enum and emit a nonsense
JSON value).
Set lombok.equalsAndHashCode.doNotUseGetters = true and
lombok.toString.doNotUseGetters = true in lombok.config so Lombok-
generated equals/hashCode/toString read fields directly instead of
routing through `this.getX()`.

Motivation: several value classes expose getters that wrap their
`@Nullable T` field in Optional<T> (ChatRequest.getToolChoice,
ChatMessage.getToolCallId) or wrap a list field in
Collections.unmodifiableList + Optional (ChatMessage.getParts) for the
public-API contract. Those wrappers are not the equality contract.

The previous getter-routing behaviour caused two real defects:

1. fb-contrib OI_OPTIONAL_ISSUES_CHECKING_REFERENCE fired on every
   Lombok-generated `this$x == null` branch when the getter returned an
   Optional. Optional is the standard "never null" type, so the null
   arm was dead code — fb-contrib correctly flagged it, contradicting
   my prior characterisation as a "false positive".

2. ChatMessage.getParts() allocated a fresh
   Collections.unmodifiableList AND a fresh Optional on every equals
   or hashCode call. With field access these allocations disappear
   entirely.

Switching is semantically identical: Optional.equals and
Collections.unmodifiableList(x).equals(...) both delegate to value-
based comparison of the underlying state. Verified by an audit covering
every Lombok-annotated class:

- Bucket 1 (verbatim getter): 11 classes — bit-identical output.
- Bucket 2 (Optional/unmodifiable wrapper getter): ChatRequest,
  ChatMessage — both benefit from the switch.
- Bucket 3 (getter does non-trivial work equality should see): 0.
- Bucket 4 (@ToString-only identity classes): unaffected.

All value classes are `final`, so subclass-override of a getter cannot
change equality. callSuper=true chains
(InferenceParameters/ModelParameters) are unaffected because the parent
classes have no getter on their own included field. No test in the
repository pins a Lombok-format `Class(field=value)` substring.

SpotBugs Max+Low: 6 -> 2 findings (the 4 OI_OPTIONAL_ISSUES_CHECKING_REFERENCE
entries on ChatRequest/ChatMessage equals + hashCode clear). The
remaining 2 findings (DLS_DEAD_LOCAL_STORE on cancelHook,
SPP_FIELD_COULD_BE_STATIC on LlamaModel.ctx) are pre-existing and
unrelated.

Full test suite: 921 tests pass (the 1 error is the environmental
RerankingModelTest.setup UnsatisfiedLinkError — no native library on
this sandbox — not a regression).
…USBR + getter routing)

End-to-end sweep that brings jllama to zero SpotBugs Max+Low findings.
The history audit while doing this surfaced that several earlier
"successful" suppressions were lost in past rebases and never actually
committed — this commit re-applies the canonical pattern in one place.

Five changes:

1. DLS_DEAD_LOCAL_STORE on LlamaModel.completeAsync — Option A from the
   cross-repo discussion: drop the `cancelHook` local entirely, lift
   `@SuppressWarnings("FutureReturnValueIgnored")` to the method. The
   inline comment documents why the suppression sits at method scope
   (cross-repo FireAndForget DLS reckoning).

2. SPP_FIELD_COULD_BE_STATIC on LlamaModel.ctx — narrow `<Match>` block
   in spotbugs-exclude.xml with rationale: `ctx` is the per-instance
   native handle, making it static would corrupt state across parallel
   LlamaModel instances.

3. RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE x2 in ChatRequest.buildMessagesJson:
   ChatMessage.getContent() and ToolCall.getArgumentsJson() are both
   @nonnull (no @nullable annotation; NullAway proves the contract);
   the `== null ? "" : ...` ternaries were dead code. Removed.

4. USBR_UNNECESSARY_STORE_BEFORE_RETURN on Lombok-generated equals /
   hashCode / canEqual / toString — restore the cross-repo canonical
   suppression block. Lombok injects the textbook polynomial-hash
   pattern (`int result = 1; ...; return result;`) on every value
   class; fb-contrib's USBR detector doesn't honour @lombok.Generated
   and would otherwise fire ~18 times across the codebase. Suppression
   matches the four Lombok-emitted method names; the cross-repo
   rationale lives in ../workspace/policies/lombok-config.md.

5. lombok.config — restore `doNotUseGetters = true` for
   @EqualsAndHashCode and @tostring. Cross-repo invariant tracked in
   the workspace policy. Without it, fb-contrib
   OI_OPTIONAL_ISSUES_CHECKING_REFERENCE fires on every Optional-
   wrapping getter routed through Lombok's generated equals/hashCode
   (ChatMessage.getParts() returns Optional<List<ContentPart>>).

Verification:
- mvn compile spotbugs:check -Dspotbugs.effort=Max -Dspotbugs.threshold=Low
  → BugInstance size is 0, BUILD SUCCESS.
- 121 tests pass across ChatRequestTest / ChatResponseTest /
  InferenceParametersTest / JsonParametersTest / MultimodalMessagesTest.

Net cross-repo SpotBugs Max+Low: 0 / 0 / 0 / 0 across jllama / BAF /
plugin / streambuffer.
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 6, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants