Skip to content

jackson2: ignore unknown fields; core: optional API key, ApiKey without a public constructor - #18

Merged
dfa1 merged 2 commits into
mainfrom
fix-codec-and-apikey
Oct 3, 2026
Merged

dfa1 merged 2 commits into
mainfrom
fix-codec-and-apikey

Conversation

@dfa1

@dfa1 dfa1 commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Two fixes found while pointing the client at a local Clef-flash server (the MLX port's clef_mlx.py serve, a TypeSafe-compatible /v1/systemone).

jackson2: ignore unknown fields, like jackson3 (fixes #16)

Jackson 2 rejects unknown properties by default and Jackson 3 doesn't, so a field the API adds to a response (here usage.latency_ms) broke jackson2 clients only. Jackson2Codec now disables FAIL_ON_UNKNOWN_PROPERTIES; a new test in both Jackson2CodecTest and Jackson3CodecTest covers unknown fields at the top level, in usage and in an answer.

core: optional API key; ApiKey without a public constructor (fixes #17)

  • Breaking: TypeSafeClient.builder() takes no argument; Builder.apiKey(ApiKey) is optional. Without a key no Authorization header is sent. There's no special case for the default endpoint: a missing key there surfaces as TypeSafeException.Authentication (401) on the first call.
  • Breaking: ApiKey is a final class with a private constructor. ApiKey.of(String) joins fromFile/fromDefaultFile/fromEnv, and the raw value is no longer exposed (only toHttpHeaderValue() and the masked toString()).
  • Every call site (cli, acceptance, tests, javadoc) and the docs move to builder().apiKey(key).

Verified

  • ./mvnw verify passes.
  • End to end: TypeSafeClient.builder().endpoint(URI.create("http://localhost:8000/v1/systemone")).build() with the jackson2 codec and no key gets typed answers from the local Clef-flash MLX server (about 0.53 s per 3-question request on an Apple M5). Before these fixes, the same call needed a made-up key and failed to decode with jackson2.

🤖 Generated with Claude Code

dfa1 and others added 2 commits October 3, 2026 14:28
Jackson 2 fails on unknown properties by default and Jackson 3 doesn't, so a
field added to an API response broke jackson2 clients only (seen with a local
Clef server adding usage.latency_ms). Both codec tests now pin the behavior.

Fixes #16

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TypeSafeClient.builder() takes no argument and Builder.apiKey(ApiKey) is
optional: without a key no Authorization header is sent, which is what a local
TypeSafe-compatible server (Builder.endpoint) needs. No special case for the
default endpoint: a missing key there surfaces as Authentication (401).

ApiKey becomes a final class with a private constructor: ApiKey.of(String)
joins fromFile/fromDefaultFile/fromEnv, and the raw value is no longer
exposed (only toHttpHeaderValue and the masked toString).

Fixes #17

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dfa1
dfa1 force-pushed the fix-codec-and-apikey branch from 3eb78bb to 88f23dc Compare October 3, 2026 12:29
@dfa1
dfa1 merged commit e2821ca into main Oct 3, 2026
1 check passed
@dfa1
dfa1 deleted the fix-codec-and-apikey branch October 3, 2026 12:30
dfa1 added a commit that referenced this pull request Oct 3, 2026
TypeSafeClient.builder(ApiKey) became builder().apiKey(ApiKey) in #18.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dfa1 added a commit that referenced this pull request Oct 3, 2026
mlx-community's 4-bit port of Clef-flash serves TypeSafe's /v1/systemone, so
the regular client (builder().endpoint(...), no API key since #18) talks to it
directly. On an M5: about 0.55 s per 3-question request, and on the 104 cached
requests 95% yes/no and 88% choice agreement with jev-1.13.0 (Laya: 85%, 64%).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dfa1 added a commit that referenced this pull request Oct 3, 2026
…LX (#14)

* local: add LocalTypeSafeClient, in-process engines on ONNX Runtime

New module typesafe-java-local: a TypeSafeClient that evaluates on ONNX Runtime
from a local model directory instead of calling the API. Engines: Laya (ModernBERT
+ decision head, recommended) and Qwen2.5-1.5B (next-token logits). Pure-Java
tokenizer and JSON; depends only on core and ONNX Runtime. Real-model tests are
tagged "model" and excluded by default. JevComparison replays 104 requests against
cached real-Jev answers; the `Local engines` workflow runs tests, comparison and
JMH on Linux/macOS and writes the tables to the job summary.

Also pins the checkstyle engine to 14.3.0: the plugin's default 9.3 can't parse
Java 21 pattern matching for switch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* local: model test asserts the anger ranking only; document int8's CPU dependence

On the macOS (M1) runner int8 Laya scored furious 2.02 vs calm 1.53, missing the
0.5 margin by 0.01. On the x86 runner int8 agreed with Jev less (78% vs 85% on
yes/no) while fp32 matched exactly, so the how-to now says when to prefer fp32.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* local: Laya fp32 from onnx-community by default; int8 opt-in with a warning

onnx-community/laya-typed-decisions-ONNX is the same export (encoder + head,
dynamo, opset 18) verified against PyTorch, so download.sh replaces exporting;
LayaEngine reads its layout (onnx/model.onnx, config.json "laya", bool
marker_mask) as well as the flat one. Agreement with Jev is unchanged (85/64/0.19).
int8 becomes opt-in: on one x86 runner it agreed less, on another it returned
flat distributions. CI downloads instead of installing PyTorch; export_onnx.py
only regenerates the PyTorch fixture.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* local: drop int8 Laya

Dynamic int8 gave CPU-dependent answers (silently flat distributions on one x86
runner), so the module ships fp32 only: no quantize_int8.py, no laya-int8 engine,
and LayaEngine reads onnx-community's layout only (config.json "laya", bool
marker_mask). CI no longer needs uv.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* local: Engines enum for the test harness; JEV cache as 5 suite files

Engines (LAYA, LAYA_GPU, QWEN, QWEN_GPU) carries each engine's model directory,
GPU flag and loader, replacing string matching; JMH takes it as a @PARAM, the
comparison and -Dengine accept "laya-gpu" or "LAYA_GPU". The 104 cached Jev
answers move from one file per request to one JSON line per request in
jev/<suite>.jsonl (5 files); the answers are unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* local: optional Laya fp16 (download.sh fp16) for the GPU

onnx-community's fp16 export: same answers as fp32 in half the download (0.85 GB),
fastest on the GPU (WebGPU/Metal), 2-3x slower on the CPU, where ONNX Runtime
converts fp16 on the fly. Engines gains LAYA_FP16/LAYA_FP16_GPU; the macOS CI job
downloads it and measures LAYA_FP16_GPU.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* local: JSON through JsonCodec; hf download instead of download scripts

The custom Json reader/writer is gone: tokenizer.json, Laya's config and structured
states go through typesafe-java's JsonCodec, found via ServiceLoader like the API
client (so jackson2/jackson3 is needed at run time). Jackson's compact JSON
measured no different against Jev than Python's json.dumps spacing; the tokenizer
fixture is regenerated for it. Onnx.model(dir) finds the one .onnx file in dir/onnx
or dir, so files keep their Hugging Face names and the bash download scripts give
way to `hf download` commands (docs, CI).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* local: Clef-flash engine, with a 4-bit conversion

ClefEngine runs Cloudflare's Clef-flash (Qwen3.5-9B + joint schema head)
from Ollaya's ONNX graph, which reads the upstream bf16 safetensors in place;
the first load hard-links them under the sha256-<hash> names the graph uses
(ONNX Runtime rejects symlinks resolving outside the graph's directory).
ClefEngineTest checks the port of encode_record token for token against
Clef's own Python (scripts/clef/reference.py).

scripts/clef/quantize_q4.py replaces the 249 projections with MatMulNBits:
20 GB -> 7.7 GB, and on an M5 2-4 s per request on WebGPU (7-13 s on CPU)
instead of minutes. CoreML was tried and rejected: ONNX Runtime's CoreML EP
supports neither MatMulNBits nor any 8-bit op.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* local: JevComparison builds its API client with builder().apiKey(...)

TypeSafeClient.builder(ApiKey) became builder().apiKey(ApiKey) in #18.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: run Clef-flash on a Mac with MLX

mlx-community's 4-bit port of Clef-flash serves TypeSafe's /v1/systemone, so
the regular client (builder().endpoint(...), no API key since #18) talks to it
directly. On an M5: about 0.55 s per 3-question request, and on the 104 cached
requests 95% yes/no and 88% choice agreement with jev-1.13.0 (Laya: 85%, 64%).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* local: one public client class per model

LocalLayaTypeSafeClient, LocalQwenTypeSafeClient and LocalClefTypeSafeClient,
each with a static load(Path), replace LocalTypeSafeClient's laya/qwen/clef
factories. LocalTypeSafeClient stays as their package-private base (model
check, timing, async, listModels), which the test harness still builds
directly for engine variants.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* local: Qwen as the prompted-LLM baseline

Measured on the same 104 cached requests: Qwen2.5-1.5B agrees with jev-1.13.0
on 80% of yes/no and 68% of choices (score error 0.26) at 2.05 s/request,
against Laya's 85%/64%/0.19 at 0.19 s and Clef-flash's 95%/88%/0.07. The docs
and LocalQwenTypeSafeClient's javadoc now present it as what prompting a
general LLM buys, not as an option; the reference table names the classes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* local workflow: download Clef-flash's tokenizer for ClefEngineTest

The parity test checks the Java prompt builder against Clef's own Python and
only needs tokenizer.json (20 MB), not the weights.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* CHANGELOG: PR number on local's entries; Qwen as the baseline

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: local's ONNX Runtime dependency, its size and platforms

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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.

core: make ApiKey optional in the builder, and hide ApiKey's constructor jackson2: reject responses with unknown fields, unlike jackson3

1 participant