fix(server): report backend errors on the Anthropic messages bridge - #3006
Conversation
fl0rianr
left a comment
There was a problem hiding this comment.
Nice fix overall, especially the sent_error handling to avoid emitting normal closing frames after an Anthropic error event.
One thing I think we should tighten up before merging is the status/error-type mapping. build_anthropic_error() currently only handles 400, 404, and 429 explicitly, so other valid backend errors like 401, 403, 413, 422, 504, or 529 end up as api_error.
There is also a streaming-specific gap: StreamingProxy may expose the backend status as error.status or not add it at all when the backend already returned a structured error object, while backend_error_http_status() only checks status_code and numeric code. That means a streaming 429/5xx can lose its original status and be translated as a generic 500/api_error.
Could we make the backend status propagation consistent, ideally using status_code, and expand the Anthropic error-type mapping accordingly? A small test for a non-400 4xx plus a streaming 429/529 case would cover this nicely.
POST /v1/messages answered 200 with an empty text block whenever the backend failed. convert_openai_chat_to_anthropic() has no error branch, so the router's error payload fell into its empty-content path, and the streaming adapter inspects only id, usage and choices[0], so it dropped the framed error event the OpenAI stream carries since lemonade-sdk#2975. Relay the failure instead: the non-streaming path answers with an Anthropic error body and the backend's own status, and the streaming path emits an event: error frame and ends the stream there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJEHxATjVbCwjcJesuFxTs
Backends reported an upstream HTTP status under two different keys, and a backend that returned its own structured error carried no status at all, so the Anthropic bridge translated a 429 or 5xx as a generic 500/api_error. The proxy now always publishes `status_code`, and the error-type mapping covers every status Anthropic names.
ec86c30 to
aeb2209
Compare
|
Both of these were real, and the second one was worse than described — thanks for pulling on it. What
|
| status | type | status | type | |
|---|---|---|---|---|
| 400 | invalid_request_error |
429 | rate_limit_error |
|
| 401 | authentication_error |
500, 502, 504 | api_error |
|
| 403 | permission_error |
529 | overloaded_error |
|
| 404 | not_found_error |
other 4xx | invalid_request_error |
|
| 413 | request_too_large |
Status extraction now also reads details.status_code, matching get_error_status_code().
On the tests — and why they are C++ rather than Python
I could not write the integration tests you asked for, and the reason is worth stating rather than quietly substituting something else. No in-repo backend can drive build_anthropic_error() to anything but 400:
- A non-400 4xx does not reach it. An unknown model 404s in the
auto_load_modelcatch atanthropic_api.cpp:996, well beforeset_anthropic_backend_error_response(); anything that throws lands in the outer catch as a hardcoded 500/api_error. The only status that reaches the mapping from a real backend is llama.cpp's 400. - 429 and 529 have no producer at all. Nothing in
server_models.jsonrate-limits or reports overload; those come from cloud providers, which need credentials CI does not have.
So the mapping was untestable where it lived. The two helpers move to src/cpp/include/lemon/anthropic_error.h and get a cpp-ci unit test — AnthropicErrorTest, 26 cases covering 401/403/413/422/429/504/529 and the status_code / details.status_code / code / type-fallback extraction order. ctest -L cpp-ci goes 25/25 → 26/26.
That header is also the answer to the open question in the PR description: backend_error_http_status() is no longer a private duplicate sitting in a .cpp. Merging it with get_error_status_code() outright is a bigger change — that one is used by ~20 handlers and has no code fallback — so I have left them separate. Happy to do that consolidation as a follow-up if you want it.
Verification
ctest -L cpp-ci: 26/26 (was 25/25).test_027/test_028and the wholeserver_streaming_errors.pysuite (7/7, includingtest_004afrom fix(server): frame backend errors as SSE events on the streaming path #2975, which asserts on this exact SSE payload): pass against a rebuiltlemondonllamacpp:vulkan+Tiny-Test-Model-GGUF.- Rebased onto
mainat756d94ba, so fix(server): return the router error status on rerank, slots and tokenize #2974 is now in the base.
(Comment drafted with AI assistance, per docs/dev/contribute.md.)
fl0rianr
left a comment
There was a problem hiding this comment.
The status propagation changes look good now, thanks. I think there’s just one mapping gap left: Anthropic currently defines 402 -> billing_error, 409 -> conflict_error, and 504 -> timeout_error. Right now 402/409 fall through to invalid_request_error, while the new test explicitly expects 504 to be api_error. Could we add those three mappings and corresponding test cases? After that this looks good to me.
Anthropic names a type for 402, 409 and 504. The first two fell through to invalid_request_error and the third to api_error, so a backend that returned one of them was reported to the client as a different class of failure.
|
Added in
Test rows added for 402 and 409, and the 504 row flipped to |
* ci: isolate llama.cpp validation cleanup on self-hosted runners (#2915)
* ci: isolate llama.cpp validation cleanup on self-hosted runners
Replace broad process-name cleanup in the llama.cpp validation job with runner-scoped process handling.
Track the lemond process, reject occupied ports and unrelated inference processes, prefer graceful shutdown, and only force-stop processes associated with the current workspace or job cache.
Keep validate_llamacpp.py and the benchmark request flow unchanged so the lifecycle fix does not alter TTFT measurement semantics.
* fix(ci): make llama.cpp cleanup idempotent
* Add Vulkan runner configuration extraction
* Reclaim routing helpers when a router collection's policy changes (#2795)
* Reclaim routing helpers when a router collection's policy changes
* Close the race condition
* Fix CI tests, load race and add one more test
* Fix load retry, replace with normal model and concurrent notifications
* Fix three issues
* Fixes
* Fix race condition
* Both fixed
* Fix
* Code refactor
* Potential fix
* Second fix
* fix: small CMake add_test_error
---------
Co-authored-by: fl0rianr <226492742+fl0rianr@users.noreply.github.com>
* [backends] Add support for image generation through TheNoise (#2927)
* implement thenoise backend
* add documentation
* add tests
* clean parameters
* address review comments
* regenerate doc
* add lora_dir validation
* remove redundant check and align defaults
* align doc
* fix typo
* fix: refactor cleanup and improve logging (#2992)
* fix(server): frame backend errors as SSE events on the streaming path (#2975)
* fix(server): frame backend errors as SSE events on the streaming path
forward_sse_stream() wrote the backend error body straight to the client
sink. The response is already committed as 200 text/event-stream, so the
body arrived without a data: prefix and every spec-compliant SSE parser
dropped it: clients saw an empty stream that just ended.
post_stream() documents its on_status hook for exactly this case and
forward_byte_stream() already uses it. Do the same here: divert the
non-200 body, then emit it as one framed data: event carrying the same
{error: ...} shape the byte-stream path produces.
* test: assert the backend's own error survives SSE reframing
The assertion only required a non-empty message, so the test would still
pass if forward_sse_stream() fell back to synthesizing
"backend returned HTTP 400" instead of forwarding llama.cpp's body — the
regression the framing change exists to prevent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgdPUX8EJ2U8V3RhwcoJkP
* refactor(server): drop the obvious comments from the SSE error path
AGENTS.md asks for a comment only where the WHY is non-obvious. Keep the
one constraint the code cannot show — the response is already committed
as 200 text/event-stream — and drop the rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmAGTDXtwhg5Lz7DwCUx8p
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* ci: cut the longest test jobs via parameter tuning (#2953)
* ci: cut the longest test jobs via parameter tuning
Trellis 3D generation (the worst offender at ~29 self-hosted minutes per
run) now launches trellis-server with classifier-free guidance disabled
via a new trellis_args recipe option, dropping a mesh from 98.5s to 31.8s
locally while still exercising every pipeline stage. Hosted-runner jobs
stop pulling Phi-4-mini (residency test now uses Llama-3.2-1B) and the
5.2 GB SD-Turbo safetensors (ollama steps use the 2 GB GGUF).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: restore trellis args after the 3D suite
/internal/set writes through to config.json, so the guidance-disabling
args the 3D suite applies would otherwise persist for every later
generation on that server -- silently degrading meshes on a developer
machine and pinning the self-hosted CI cache dirs to the fast path even
after the test stops asking for it. Save and restore around the suite,
and tolerate a server that predates the trellis_args option.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: pass the 3D fast-generation args per load
Setting trellis.args through /internal/set wrote through to config.json,
so the guidance-disabling args outlived the suite: a developer's server
kept serving degraded meshes, and an interrupted job left the persistent
self-hosted cache dir pinned to the fast path. POST /load carries recipe
options for one load without save_options, so nothing is persisted and
the generation reuses that process; unload afterwards so no guidance-free
backend is left resident.
Also validate custom args before the backend install can download an
archive, and reserve --res alongside the other Lemonade-managed flags.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: drop the unused server config getter
The per-load approach reads no config, so this helper has no callers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: give the 3D load its own timeout and unload only its model
The explicit /load can install the trellis backend, which downloads and
extracts an archive; it was inheriting the 500s model-operation budget
where the lazy path had 1800s, so a cold runner would report an
environment stall as a product failure. Unload by name too, so a suite
run against a developer's own server leaves their other models resident.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: unload the 3D model through an auth-aware helper
The hand-rolled unload skipped the Authorization header every other
helper sends and ignored the response, so against a key-protected server
it would 401 in silence and leave the guidance-free backend resident.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(trellis): reserve the -m alias for --models
trellis-server documents -m as the short form of --models, and custom
args are appended after the managed ones, so trellis_args of "-m <dir>"
would win and silently point the backend at a different model directory
while lemond reported the requested model as loaded. Peer backends
reserve both spellings.
Load through an auth-aware helper too, matching the unload beside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: assert the 3D speed-up args reached the backend
An option the server does not recognize is dropped during recipe-option
resolution, so a rename or a typo would still return 200 from /load and
quietly restore the slow full-guidance path -- the whole point of this
suite's tuning -- with nothing failing. Read the applied options back
from /health and compare.
Also reject an empty name in unload_model(), which the server reads as
"unload everything".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: compare trellis arg tokens instead of the raw string
A server that already carries trellis args merges them with the ones the
request supplies and rebuilds the value alphabetically, so an exact-string
comparison reports "never reached the backend" for a value that did in
fact arrive. Compare tokens instead.
Give load_model the standard model-operation timeout as well; 30s is
shorter than a cold load can legitimately take.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: isolate the 3D load from host trellis args
Pass merge_args=False on the preload so trellis args already configured on
the machine cannot alter the timing the suite is tuned for. With no merge
the applied value is the request string verbatim, so assert it exactly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* docs: add new engine logos to homepage engine ticker (#2920)
* docs: add vLLM, Moonshine, OpenMOSS, TRELLIS, ACE-Step to engine ticker
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(docs): make engine ticker loop seamless and reach all logos
The ticker track was a block-level flex container, so its width was clamped
to the container instead of its content. translateX(-50%) therefore only
scrolled 560px of a ~2160px logo strip before snapping back, which both
looked janky and made the trailing logos unreachable. Size the track to
max-content and move spacing from flex gap onto the chips so the -50%
keyframe lands exactly on the duplicated copy's start.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: update Moonshine upstream link to moonshine-ai org
The repo was transferred from usefulsensors to moonshine-ai; the old URL
301-redirects. Aligns the backend doc with the homepage engine ticker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: fl0rianr <226492742+fl0rianr@users.noreply.github.com>
* Show all local model versions in one folder (#2107)
* fix: show all local model versions in one folder
List different GGUF versions, such as Q4 and Q8, from the same local model folder as separate choices.
Keep the old folder name working for existing scripts, and keep sharded models and mmproj vision files grouped correctly.
* Fix extra model folder review issues
Show split extra model folders as variant models only, while keeping the old folder name working in requests. Also handle folders with multiple sharded variants and always pick the same mmproj file.
* fix(server): don't let one extra model folder overwrite another
Two folders with the same GGUF filenames produced the same model id, so
the one read second replaced the first. Models now go through
add_extra_model(), which never overwrites: a clashing name gets its
folder name added.
Adds a regression test.
* fix(server): only group GGUF files that are really parts of one model
Files were grouped by the quantization tag in their name, so
Model-Q4_K_M.gguf and Model-Q4_K_M-imatrix.gguf looked like two halves
of one split model and one became unreachable.
Files now group only when their names say they belong to the same split
set. Same code backs the registry variant list, so that changes too.
Adds a regression test and updates Extra-Models-Dir-Spec.md.
* fix(server): sort GGUF variants by quant token, not display name
Names widen to file stems when two variants share a quant, but sorting
still looked the name up in the quant priority table. Widened names miss,
fall into the unranked bucket, and Q8_0 sorts ahead of Q4_K_M — so
`pull --yes` picked the wrong default. Track the quant separately on
GgufVariant and sort on it; names stay widened for unique selection.
Add test/cpp/test_hf_variants.cpp (HfVariantsTest) covering the
Q4 + Q4-imatrix + Q8 case; it fails against the previous comparator.
* Enable CI for HfVariantsTest in CMakeLists
---------
Co-authored-by: Andi M <webmaster@anditherobot.com>
Co-authored-by: fl0rianr <226492742+fl0rianr@users.noreply.github.com>
* test: run the committed model-type classifier test in CI (#2976)
test/cpp/test_model_type_classifier.cpp and test/cpp/test_ggml_hip_path.cpp
were committed but referenced by nothing, so neither was ever built or run.
Register the classifier test, which covers get_model_type_from_labels() and
passes 17/17 against the current implementation. Remove the HIP path test:
llamacpp backend without a header declaration, so the file has not compiled
since June - which went unnoticed precisely because nothing builds it.
Also point the testing guide at add_cpp_ci_test(); #2877 made it the entry
point and AGENTS.md now forbids calling register_cpp_ci_test() directly.
* fix(server): return the router error status on rerank, slots and tokenize (#2974)
* fix(server): return the router error status on rerank, slots and tokenize
Router::reranking, get_slots, slots_action and tokenize return an error
payload instead of throwing when the loaded backend cannot serve the
request. Their handlers wrote that payload out with HTTP 200, so clients
read a failure as a success. #2061 added the set_error_response() check to
handle_embeddings; apply the same check to the four handlers it missed.
* fix: existing false test error report
Handle 501 status code for slots erase endpoint and assert error details.
* test: reformat the slots error assertion with the pinned black
The assertion added in cef16cf1 is 92 characters, so `black` 26.1.0 --
the version pinned in .pre-commit-config.yaml -- would rewrite it. No
workflow runs black, so this never surfaced in CI; the reformat keeps
the branch pre-commit clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MXUsoR4SFdzL4qu7S3n9DZ
---------
Co-authored-by: fl0rianr <226492742+fl0rianr@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* ci(test): speed up server job suite (#3007)
* Add complete vision benchmark support (#2869)
* feat(bench): add vision benchmark support for image-capable models
Add ability to benchmark vision (multimodal) models by embedding images
into chat messages during text generation benchmarks. Vision scenarios
specify an image_path and messages; the CLI loads and base64-encodes the
image, then injects it into user message content arrays using the OpenAI
multimodal format before sending to the server.
Only allow vision-capable models to run vision scenarios
Only load image files from the scenario file directory for security
Source images for the tests taken from public domain sources:
- https://en.wikipedia.org/wiki/United_States_Declaration_of_Independence
- https://www.nro.gov/foia-home/
- https://ntrs.nasa.gov/
- https://www.cia.gov/readingroom/
Lots of challenging formatting, interesting fonts, and poor image quality.
The scan of a fax of a photocopy of a microfiche of a mimeograph image path
can really challenge the transcription models.
Co-authored-by: opencode, North-Mini-Code (512k)
Co-authored-by: opencode, Qwen3.6-35B-A3B (256k)
* address review feedback
* remove benchmark images for now
they'll probably reappear somewhere else, once we figure out this whole
benchmark ecosystem in a way that lets people compare all kinds of systems.
The vision scenarios are documented in the CLI guide for those who wish
to try it.
---------
Co-authored-by: Michele Balistreri <michele@bitgamma.com>
* ci: cut installer build time on the PR/merge-queue critical path (#2989)
* ci: cut installer build time on the PR/merge-queue critical path
Three independent wins on the two jobs that gate everything downstream:
- MSVC compiled one file at a time per project, so the Windows C++ build
used a quarter of the runner. Enable /MP for Visual Studio generators.
- The Tauri desktop app is ~8 min of the installer build and only lands in
lemonade.msi, which no test job installs. Build it in a packaging-only
job instead.
- The Debian package build built ~115 unit-test binaries and discarded
them, then the next step rebuilt them from scratch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci: run the C++ unit tests in their own job
They shared nothing with the Debian packaging build in the same job — a
separate configure with different flags — so their 4.5 min landed directly
on the critical path that gates every .deb test job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci: let MSBuild overlap independent Windows projects
lemonade-server-core takes 5.2 min and the CLI and LemonadeServer, which
do not depend on it, waited for it to finish. Pass /m through to MSBuild
rather than via --parallel, which would set CL_MPCount=1 and cancel /MP.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci: ship a primed compiler cache in the build-environment image
GitHub Actions caches are scoped per git ref, so merge-queue runs — which
every merge passes through — can never restore one. Priming the nightly
build image instead gives the .deb build a warm cache everywhere.
Primed at the container checkout path so the absolute include paths on the
compiler command line match, and on amd64 only: nothing consumes the arm64
image, and priming it would run the build under QEMU. Release builds opt
out, because the version-dependent -fdebug-prefix-map has to leave the
ccache hash for anything to hit.
container-build-test.yml now builds this image too — build-container.yml
publishes it on a nightly schedule, so changes were previously unexercised
until they hit the tag every Linux CI job pulls.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): stop the ccache prime at the build target
The install step wants examples/, which .dockerignore strips from the build
context. Only the compile populates the cache, so packaging was never
needed — and skipping it makes the image build faster too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(ci): tighten the build-time changes
- Pin -fdebug-prefix-map with DEB_BUILD_DEBUGPATH instead of dropping it from
the ccache hash. dpkg honours it over the changelog-derived path, so cached
objects now carry correct debug paths rather than a stale version.
- Add cpp-unit-tests to backends-gate. Moving the tests out of the .deb job
had removed them from the only gate that enforced them in the merge queue.
- Extract the duplicated WiX install into .github/actions/install-wix; the
second copy had already lost the x86 fallback and the version check.
- Share one FetchContent cache namespace across the Windows installer jobs —
both run bare setup.ps1, so a separate namespace only guaranteed a miss.
- Make add_cpp_ci_test fail under BUILD_TESTING=OFF, so a test block that
forgets the guard is caught instead of silently slowing packaging builds.
- Reuse the ci:distros label rather than adding a second label to the gate it
reports through, and document the job in the deferral table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci: stop building discarded unit-test binaries in the RPM job
setup.sh configures with testing on and the build has no --target, so all 43
test executables were built and then left out of the .rpm — the same waste
BUILD_TESTING=OFF removed from the Debian path, across a four-leg matrix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): address review of the container prime and Windows jobs
- Prime only the image tag whose compile CI gates (24.04), via a PRIME_CCACHE
build arg. Priming 25.10/26.04 meant a newer-toolchain break would fail the
nightly publish and silently strand consumers on a stale image.
- Record the pinned debug path in /opt/ccache-debugpath from the prime itself
instead of an unconditional ENV, so an unprimed image (arm64, or one built
before this) cannot advertise a cache it does not have.
- Bind-mount the source instead of COPY, so the tree stops shipping as a layer
in an image every Linux CI job and every PPA job pulls.
- Move the image build onto build-container.yml with a narrow paths filter.
In container-build-test.yml it fired on every C++ PR — a long uncached job
added to nearly every PR, against the concurrency cap this PR exists to ease.
- Upload lemonade.msi from the desktop job. release-v* pushes used to get it
from the combined build and were left with no desktop installer artifact.
- Extract the Windows CMake fallback into a composite action and use it in all
three Windows jobs; the new job had silently skipped it.
- Cap MSBuild at /m:2. With /MP already spawning one cl.exe per core inside
each project, the two multiply to 16 compilers on a 4-core runner.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): give each WiX installer its own web-app fragment
wix_installer_minimal and wix_installer_full both regenerated the same
WebAppFragment.wxs and then fed it to wix build, so building the wix_installers
aggregate in parallel let one rewrite the file while the other read it — an
intermittent failure on the release and signing path only.
Also from review:
- cpp-unit-tests installs build-deps again. As a step of build-lemonade-deb it
inherited that job's build-dep install; standalone it would have failed on a
newly added Build-Depends until the nightly image caught up.
- Verify the primed cache on the publish path too, against the pushed tag. It
only ran on the PR path, so a prime that produced nothing would ship.
- Drop the buildx layer cache from build-container.yml: this workflow is not
latency sensitive, and retaining a ~300 MB ccache layer per Ubuntu release
would evict the FetchContent caches that are on the critical path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): harden the ccache prime and the parallel WiX build
- Seed the WiX extension cache in the install action. `wix build -ext`
populates a shared per-user directory on first use, so the two installer
targets would race for it once /m:2 let them run concurrently — on the
release and signing path only.
- Never let a failed prime block publishing the build image. The prime
compiles a .dockerignore-filtered tree, so it can fail on a change the .deb
job accepts; blocking the publish would silently strand every consumer on a
stale image. It warns instead, and the verify step is what turns it red.
- Warn when a primed image yields zero cache hits, which is what a drifted
workspace path would look like. Previously the entire speedup could be lost
with a green, merely slower, build as the only symptom.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): pin the WiX extension and stop the two Windows jobs racing to save
- `wix extension add` without a version resolves the newest package on NuGet,
which the pinned 5.0.2 CLI would then refuse to load — a break needing no
change on our side. Pin it to the CLI version.
- Make the desktop job's FetchContent cache restore-only. Sharing the namespace
is what makes it hit; sharing the save just races the installer job for the
same key and logs a reservation failure on every cold run.
- Correct the paths-filter comment, which claimed a coverage guarantee the
.dockerignore-filtered prime does not actually provide.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): move the image layer cache to the registry, document the guard
- Restore buildx layer caching via ghcr rather than the Actions cache. Dropping
it entirely made the nightly re-run apt build-dep under QEMU for every arm64
leg with no reuse; ghcr has neither the 10 GB cap nor the LRU eviction that
would have cost the FetchContent caches on the critical path.
- Document the BUILD_TESTING guard that add_cpp_ci_test now enforces, in the
helper's own usage block and in docs/dev/testing.md — which also still named
the internal register_cpp_ci_test().
- Check the primed cache's size before its marker file, so a prime that dies
partway reports the explanatory error instead of a bare `cat: No such file`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): keep a Rust compile canary on the PR path
Deferring the Windows desktop installer left no PR-path job compiling
src/app/src-tauri: the macOS .dmg is ci:macos-gated and contrib/debian/rules
never sets BUILD_TAURI_APP. A broken Tauri host would have gone green and
failed only once the PR was queued. Docs And Style now cargo-checks it — a
compile canary, not a packaging build.
Also from review:
- Build wix_installers serially. It is the only target owning two concurrent
`wix build` runs and is built only on the release and signing paths, so a
parallel configuration there would first run for real during a release.
- Pin the WiX extension version where `wix build` uses it, not only in the CI
pre-seed, so an unversioned resolve cannot pick up an incompatible major.
- Write GITHUB_PATH as UTF-8 in the new CMake action; `>>` under PowerShell 5.1
emits UTF-16LE, which would silently garble the entry on a runner that
actually takes the install branch.
- Opt release-v* pushes out of the pinned debug path too: they also produce
.deb and -dbgsym artifacts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): derive the WiX extension pin and separate the installer obj dirs
- Pin the UI extension to the CLI version CMake actually detected instead of
a literal 5.0.2. The project supports WiX 5.0+, so a hardcoded pin would
make a newer supported CLI fail to load the extension it was handed.
- Give each installer its own -intermediateFolder. Splitting the web-app
fragment was not sufficient: both targets compile Product.wxs, so they also
shared obj/Product.wixobj with different -d IncludeTauriApp values. CI avoids
this with /m:1; this fixes it for anyone building the targets in parallel.
- Make the prime-verification failure state plainly that the image published
successfully and only the cache is missing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): compile-check the Tauri host on Windows too
A Linux-only cargo check missed the platform-gated host code: webview_shim.rs
and lib.rs both branch on cfg(target_os = "windows"), and webview2-com/windows
are Windows-only dependencies. That is precisely the code whose packaging job
this PR defers, so the canary did not cover the gap it was added for. Now a
two-platform matrix in its own job.
Also from review:
- Drop the cargo cache. A cold check measures ~1.5 min, so it bought little
while contending for the same 10 GB Actions pool this PR argues elsewhere
must be kept clear for the FetchContent caches.
- Drop the `# syntax=docker/dockerfile:1` directive. BuildKit's built-in
frontend already supports RUN --mount, and the directive makes every image
build pull a frontend from rate-limited Docker Hub.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: document the BUILD_TESTING guard in AGENTS.md
The add_cpp_ci_test example showed the unguarded pattern, which now trips the
configure-time check that keeps distro packaging from rebuilding the test
suite it discards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): make the Tauri canary binding and stop a diagnostic gating the .deb
- Move the canary into the packaging workflow and into `Packaging builds`'
needs. As two fresh job names in Docs And Style it could go red without
blocking anything until someone registered them in branch protection —
which is the late failure it was added to prevent.
- Report the ccache hit rate after the .deb upload, not before. A diagnostic
running under `set -euo pipefail` sat between the build and the upload, so
an unreadable cache would have skipped the artifact and failed four
downstream jobs with "artifact not found". This file already codifies the
opposite convention for the macOS .pkg upload.
- Apply BUILD_TESTING=OFF to the Arch/openSUSE builds too; they had the same
build-and-discard waste already fixed for Debian and Fedora.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): serialize the desktop installer build
wix_installer_full depends on both web-app and tauri-app, and both touch
src/app: web-app robocopies the whole tree while tauri-app runs npm ci inside
it. /m:2 let them run concurrently, so robocopy could hit files npm was
rewriting and retry under its default /R:1000000 /W:30 — hanging the job to
the workflow timeout rather than failing. /m:2 stays on wix_installer_minimal,
whose graph has no src/app writer.
Also note in the canary's comment that cargo check covers neither linking nor
asset embedding, so the boundary is written down rather than assumed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(server): report backend errors on the Anthropic messages bridge (#3006)
* fix(server): report backend errors on the Anthropic messages bridge
POST /v1/messages answered 200 with an empty text block whenever the
backend failed. convert_openai_chat_to_anthropic() has no error branch, so
the router's error payload fell into its empty-content path, and the
streaming adapter inspects only id, usage and choices[0], so it dropped the
framed error event the OpenAI stream carries since #2975.
Relay the failure instead: the non-streaming path answers with an Anthropic
error body and the backend's own status, and the streaming path emits an
event: error frame and ends the stream there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJEHxATjVbCwjcJesuFxTs
* fix(server): map backend status onto Anthropic error types consistently
Backends reported an upstream HTTP status under two different keys, and a
backend that returned its own structured error carried no status at all, so
the Anthropic bridge translated a 429 or 5xx as a generic 500/api_error.
The proxy now always publishes `status_code`, and the error-type mapping
covers every status Anthropic names.
* fix(server): add the billing, conflict and timeout Anthropic error types
Anthropic names a type for 402, 409 and 504. The first two fell through to
invalid_request_error and the third to api_error, so a backend that returned
one of them was reported to the client as a different class of failure.
* apply debian package cmake test fix
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: fl0rianr <226492742+fl0rianr@users.noreply.github.com>
* docs: add Muse Glimmer 30B blog post (#3033)
Announces Meta's Muse Glimmer 30B and documents running it with Lemonade:
pinning llama.cpp per backend, pulling the Unsloth 4-bit GGUF, and the four
ways to use the loaded model. Also refreshes stale install_options.html links
across the site to the current install guide URL.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: add GITHUB_TOKEN / GH_TOKEN support to GitHub API requests (#2995)
* fix: add GITHUB_TOKEN support to GitHub API requests to avoid 403 rate-limiting
Unauthenticated GitHub API calls are capped at 60 requests/hour, causing 403s under typical usage (BackendManager scans multiple repos on startup). Add a github_api_headers() helper that reads GITHUB_TOKEN / GH_TOKEN from the environment and injects an Authorization header, raising the limit to 5 000 req/h.
Applied in fetch_latest_github_tag(), resolve_asset_wildcard(), and recipe_import.cpp — the three code paths that issue HTTP GET to api.github.com.
* fix: address PR #2995 review — CI ON, retry on 401/404, fix empty-token test
- CMakeLists.txt: switch GithubApiTest to CI ON per project testing guide
- github_api.h: retry without auth on 401/403/404 (scoped GITHUB_TOKEN
can surface as any of these on unrelated repos)
- test_github_api.cpp: set_env() now sets empty strings on POSIX too;
test case 4 sets GITHUB_TOKEN="" explicitly to cover the empty-string
fallback branch instead of unsetting it
* ci: fail fast in merge queue matrices (#3014)
* feat(rocm): enable AMD Instinct MI100 (gfx908) and MI200 (gfx90a) in llama.cpp ROCm (#2092)
* feat(rocm): add gfx908 (MI100) and gfx90a (MI210) GPU support
Adds AMD Instinct MI100 (CDNA1/gfx908) and MI200/MI210 (CDNA2/gfx90a)
to the lemonade backend, following the llamacpp-rocm nightly build
support added in lemonade-sdk/llamacpp-rocm#103.
- Add gfx908 and gfx90a to ROCM_ARCH_MAPPING with both the direct arch
string (used via HSA/WSL path) and the KFD-computed variants (gfx9008,
gfx9010) produced by the native Linux digit-only parsing path
- Extend the gfx arch regex from \d{4} to [0-9a-f]{3,4} to match
3-char and alphanumeric arch strings like gfx908 and gfx90a
- Add MI100/MI200/MI210/Arcturus/Aldebaran marketing name recognition
as a fallback in identify_rocm_arch_from_name
- Register gfx908 and gfx90a as supported families for llamacpp rocm
and sd-cpp rocm backends
- Add human-readable device family names for both new architectures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(rocm): address Copilot review feedback for gfx908/gfx90a detection
- Move CDNA1/CDNA2 name checks before the early radeon/amd guard so bare
device names like "MI100", "Arcturus", or "MI250" are detected correctly
- Add mi250/mi250x to CDNA2 detection (also gfx90a)
- Fix double-space formatting in DEVICE_FAMILY_NAMES entries
- Update gfx90a human-readable name to include MI250
https://claude.ai/code/session_01Uy8Wa9tf9vXKXY1jnSqZH1
* test(rocm): add CPU-runnable unit tests for ROCm arch mapping
Follows the same pattern as test/test_cuda_arch_mapping.py. Covers:
- KFD decimal gfx_target_version -> gfx arch (90008->gfx908, 90010->gfx90a)
- Direct gfx token extraction from device name strings
- CDNA1/CDNA2 marketing name fallback incl. bare names (MI100, MI250X)
- RDNA2/3/4 and iGPU name fallback
- Non-AMD names returning empty string
https://claude.ai/code/session_01Uy8Wa9tf9vXKXY1jnSqZH1
* test(rocm): rename ROCM_SUPPORTED_ARCHS to ROCM_SUPPORTED_FAMILIES
The set mixes concrete arch strings (gfx908, gfx90a) and wildcard family
tokens (gfx103X etc.), so "ARCHS" was misleading. Add a comment explaining
that RDNA family tokens are only reachable via name-based detection, not
the KFD path which produces exact arch strings like gfx1030.
https://claude.ai/code/session_01Uy8Wa9tf9vXKXY1jnSqZH1
* feat(rocm): enable AMD Instinct MI100 (gfx908) and MI200 (gfx90a) in llama.cpp ROCm
The Linux ROCm build already carries gfx908/gfx90a kernels (GPU_TARGETS in
lemonade-sdk/llama.cpp release.yml) and the nightly repo publishes per-arch
assets; only the descriptor support list gated them out. The Windows ROCm
build omits CDNA from its GPU_TARGETS, so both arches carry a Linux-only
install gate.
Addresses review feedback on #2092: drops the name-based CDNA fallback
(Linux detection goes through KFD gfx_target_version, which already yields
gfx908/gfx90a directly) and the Python test that only exercised a replica of
the C++ logic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(rocm): address review comments on llamacpp descriptor
- Simplify ROCm description to 'AMD GPUs supported by ROCm'
- Sort gfx arch list alphabetically
- Add Linux-only gate for gfx942 (MI300X lacks Windows binaries)
- Condense comments
* fix(rocm): group DEVICE_FAMILY_NAMES gfx9* entries together
Align source order with alphabetical grouping: gfx9* first (as gfx09 < gfx10),
then gfx10*, gfx11*, gfx12*.
* docs: regenerate backend docs for ROCm description change
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* feat(router): report estimated cost on collection.router decisions (#2763)
* feat(router): attach illustrative cost metadata to routing decisions
Wire a CostServices seam into RoutingPolicyEngine so that once route_to
is resolved (matched rule or default), per-candidate cost/latency info
(typed ModelInfo fields when known, hand-authored extras otherwise) is
merged into Decision::outputs.estimated_cost. Reporting only — this
does not change candidate selection, just surfaces cost visibility on
collection.router decisions the same way it's already shown on
/v1/models.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(router): address PR #2763 review feedback
Make cost attachment exception-safe by catching exceptions from cost_of,
preserve existing estimated_cost values, improve try_get_model_info()
error handling, and add tests for these scenarios.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(router): finish remaining PR #2763 review feedback
Replace the <0-means-unknown sentinel in resolve_cost_info with
std::optional<double>, add CostInfo::to_json() to remove the duplicated
field mapping, memoize CostServices::cost_of per candidate to avoid a
registry lookup on every routing decision, validate cost_tier against
free|low|medium|high, log cost_of failures once per candidate instead of
per-request, and add tests covering typed+extras precedence and the
rejected cost_tier case.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Allow explicit system llama.cpp backend (#3016)
* Allow explicit system llama.cpp backend
* Reuse platform name for backend validation
* fix(cmake): guard runtime config test with BUILD_TESTING
---------
Co-authored-by: fl0rianr <226492742+fl0rianr@users.noreply.github.com>
* feat(server): add model alias system with /v1/models listing and /internal/aliases endpoints (#2818)
* feat(utils): add normalize_model_name helper in model_name_utils.h
* feat(server): implement standalone AliasManager with aliases.json persistence
* feat(server): decouple aliases from ModelInfo/ModelManager and update server routing & quad-prefix model listing
* feat(cli): update lemonade alias CLI commands for standalone AliasManager
* docs(aliases): add documentation for Model Alias system, CLI, REST endpoints, and invariants
* test(aliases): update unit and integration tests for AliasManager and clean up stray test diffs
* fix(aliases): address review feedback with cycle hardening, universal endpoint resolution, and doc alignment
* fix(build): declare ModelAliasTest with add_cpp_ci_test and sync generated docs boilerplate
* fix(cli): add lemon::utils::url_encode helper to avoid non-standard httplib::detail dependency
* docs: sync generated backend documentation boilerplate
* fix(server): sum sibling shards when computing on-disk GGUF size (#2973)
- Reject index > total in is_gguf_shard_filename() so the detector, not
same_shard_family(), owns shard-number validation.
- Accept the . and _ shard separators already recognized by
hf_variants.cpp and visible_extra_variant_name().
- Expose sharded_gguf_size_bytes() so tests exercise the production
aggregation instead of reimplementing it.
- Cover the model size / /models/{id}/files split end-to-end.
* fix(tts): stop dropping response_format on the streaming path (#3029)
* fix(tts): stop dropping response_format on the streaming path
/audio/speech hardcoded response_format = "pcm" for any streaming
request and never read the value the client sent. An explicit
response_format was silently dropped, and wav-only backends could not
stream at all: the forced pcm is not in their supported set, so the
request was rejected outright.
Transport and container are orthogonal -- stream_format picks the
transport, response_format the container -- so an explicit
response_format now wins on both paths, and is rejected rather than
silently swapped when the backend cannot encode it. Only the implicit
default still varies by transport, and it yields to whatever the backend
declares.
supported_streaming_audio_formats() lets a backend narrow its streaming
set. Kokoros forces headerless s16le on that path whatever
response_format asked for, so honouring anything else there would serve
bytes the Content-Type does not match. Its buffered set drops aac and
flac too, which it deserializes but silently answers with MP3 bytes.
* fix(tts): forward the resolved response_format to the backend
The gateway resolved an effective response_format and set the
Content-Type from it, then forwarded the request untouched. A backend
that received no response_format therefore fell back to its own default
and could answer with bytes the declared Content-Type did not describe --
the mismatch this endpoint is meant to prevent.
* test(tts): cover streaming from a wav-only backend
The regression this branch fixes is that a wav-only backend could not
stream at all, but nothing exercised the successful path. Stream from
OpenMOSS and assert the transport delivers a WAV container rather than
being forced to pcm and rejected.
* docs(api): describe TTS streaming as transport, not format
The /v1/audio/speech reference still named Kokoros as the backend, listed
one fixed set of response_format values, and said stream_format=audio
outputs pcm. Formats are per-backend and stream_format now selects only
the transport, so say that instead.
* fix: honor custom backend binary environment variables (#3004)
* fix: honor custom backend binary environment variables
* fix: share backend binary override resolution
* Enable CI for BackendUtilsGpuEnvTest
* fix: make backend env test compatible with gcc 11
---------
Co-authored-by: hogeheer <hogeheer@users.noreply.github.com>
Co-authored-by: hogeheer499-commits <hogeheer499-commits@users.noreply.github.com>
Co-authored-by: fl0rianr <226492742+fl0rianr@users.noreply.github.com>
* Update llama.cpp to b10360, rocm-stable to b10362, rocm-nightly to b1309 (#3053)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Add support for a default model source (#3036)
* Add support for a default model source
The default will be HuggingFace.
If a user is in China they can set the default model source to Modelscope
they can change the configuration option.
* Address code review feedback
* Address second round of code review feedback
- apply_default_pull_source now follows the ModelManager checkpoint
precedence (checkpoints.main when present, else checkpoint), so it can't
resolve provenance from one checkpoint while downloading another.
- Normalize every registry-backed checkpoint (not just main) and enforce a
single registry per model.
- Reject a source/registry_source that conflicts with a checkpoint provider
URL, and checkpoints that disagree on the registry, with 400 — matching
the CLI and /pull/variants.
- Drop remaining docs wording that described Hugging Face as the
unconditional default; document server-side URL normalization/conflicts.
- Add C++ and endpoint regression coverage for the above.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pull): reject URL/source conflicts consistently and validate explicit sources early
- Validate source/registry_source before URL normalization in apply_default_pull_source()
so invalid values (e.g. 'nexus' with an HF URL) are rejected not silently overwritten.
- Detect and reject URL vs source mismatches in /pull/variants (matching /pull and CLI).
- Remove duplicated explicit_registry_source_from_url in main.cpp; use shared detect_registry_url().
- Add URL vs --source conflict detection in CLI handle_pull_command.
- Add tests for /pull/variants conflicts and invalid source rejection.
* fix(pull): validate non-string sources and canonicalize variant aliases
Reject present-but-non-string source/registry_source before URL
normalization so an invalid value can't be silently overwritten by a
provider URL's registry; treat JSON null as absent. Canonicalize the
/pull/variants source param through parse_remote_registry_source() so
accepted aliases (e.g. hf) aren't rejected against a matching URL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(llamacpp): auto-detect draft GGUF companions (#3051)
* feat(llamacpp): auto-detect draft GGUF companions
* adress review and add dflash
* fix(llamacpp): decouple DFlash discovery from activation
* Bump project version from 11.5.2 to 11.6.0 (#3085)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(models): add Muse Glimmer 30B to the model catalog (#3090)
* feat(models): add Muse Glimmer 30B to the model catalog
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(models): enable DFlash draft decoding for Muse Glimmer
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(models): correct Muse Glimmer size to GiB
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: regenerate backends reference for Muse Glimmer
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Update llama.cpp to b10375, rocm-stable to b10397, rocm-nightly to b1311 (#3097)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* bump thenoise version (#3079)
* docs(backends): drop the model catalog tables from the backend reference (#3103)
The `## Models` section duplicated `server_models.json` as ~285 lines of
markdown tables (58% of the file) that nothing links to. The user-facing
catalog at lemonade-server.ai/models.html fetches `server_models.json` live
from GitHub raw at the release tag, so it never read these tables.
Every model-catalog PR had to rebuild lemond in the `backend-docs-drift` job
and regenerate the doc, or fail CI — #2495 was a PR whose entire content was
three table rows added to fix that failure.
Removes the section, `render_models`, the now-unused `SERVER_MODELS` constant,
and the `backend-models` marker region from the template. The descriptor-backed
regions (overview, support matrix, recipe options) are unchanged; those are
derived from C++ and genuinely need the drift check.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(alias): report the real reason for alias validation failures (#3111)
* fix(alias): report the real reason for alias validation failures
Alias handlers routed failures through create_model_error(), which rewrites its
message whenever the name is absent from the registry — always true for a new
alias — so every validation error reported "model not found" plus a list of
unrelated models instead of the actual cause.
The CLI compounded this: extract_server_error_message() only unwrapped
string-shaped error bodies, so object-shaped ones (what the server actually
emits) printed only "Request failed: <code>". That affected every CLI command
hitting a structured error, not just aliases.
Also corrects the Muse Glimmer post, which promised built-in availability on a
specific weekday and date; it now refers to the version instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(alias): classify internal alias failures as server errors
create_alias_error() hardcoded invalid_request_error, so the three 500 paths
reported a server-side failure as a client request error.
Reported by fl0rianr in review.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(server): add model registration endpoint (#3118)
* feat(server): add model registration endpoint
* feat(server): add model registration endpoint
* small name catch
* Debian packaging improvements (#3039)
* Drop quilt from build-depends (Closes: #1143219)
* d/control: reformat using cme
* d/copyright: Remove unnecessary licenses flagged with cme
* d/control: bump standards version
* docs: add winget and Homebrew install to homepage quickstart (#2970)
Co-authored-by: Jeremy Fowers <80718789+jeremyfowers@users.noreply.github.com>
* Trigger snap release candidate build from release branches (#2908)
* Trigger snap release candidate build from release branches
When a release-v* branch is pushed, dispatch snap-rc-build.yaml in the
sibling lemonade-server-snap repo so a release candidate snap is built
from that branch and published to the candidate channel.
Requires a SNAP_RC_DISPATCH_TOKEN secret (PAT with Actions:write on
lemonade-sdk/lemonade-server-snap) to be added to this repo, since the
default GITHUB_TOKEN cannot dispatch workflows in other repos.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Also trigger lemonade-desktop snap RC build in lemonade-snap repo
Dispatch snap-rc-build.yaml in lemonade-sdk/lemonade-snap alongside
the existing lemonade-server-snap dispatch, so the lemonade-desktop
snap is also built and released to the candidate channel.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: fl0rianr <226492742+fl0rianr@users.noreply.github.com>
Co-authored-by: Jeremy Fowers <80718789+jeremyfowers@users.noreply.github.com>
* fix(server): compare artifacts, not commit SHAs, when checking for model updates (#3073)
* fix(server): compare artifacts, not commit SHAs, when checking for updates
check_for_model_updates flagged an update whenever the upstream commit SHA
moved, so a README-only commit -- or any commit touching an unrelated variant
in a shared multi-artifact repository -- re-raised "Update available" on every
restart. download_from_registry already handles this via
can_reuse_previous_hf_snapshot; the startup check never called it.
- Compare the per-model artifact set (resolved checkpoints, expanded across
GGUF shard families) between the cached ref and the latest commit before
advertising a re-download.
- Gate on Hugging Face: ModelScope snapshots are tree fingerprints with no
commit pin, so they keep the snapshot-id comparison.
- Every indeterminate path (missing file, fetch failure, empty set) falls back
to flagging an update, so a real update is never hidden.
Fixes #2542
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(server): track update-check baselines separately, share artifact selection with pull
- compare artifacts against the on-disk snapshot (resolved path/refs main),
never the processed-at-pull sha, which may name a snapshot never
materialized locally
- select artifacts from the new revision's tree via the same helper pull
uses, so added-in-directory and tokenizer/config-only changes are detected
- verify auxiliary checkpoints under their own repository entries; a model
is verified only once every repository it spans completes a determination
- an indeterminate repository (no resolvable local baseline) assumes changed
instead of silently skipping, so it can no longer block another
repository's completed determination for the same model
- any indeterminate artifact check consistently assumes changed
- move the per-model repository count into registry_files::DeterminationTracker,
guarded against a model being marked determined twice for the same repository
- compare the union of the previous and current revision's selected files, not
just the current one, so a file removed upstream (e.g. from a directory
checkpoint or a GGUF shard family) isn't silently dropped out of comparison
- apply the same union fix to pull's own snapshot-reuse check
(download_from_registry), which shared the identical blind spot independently
of this update-check path
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(server): keep same-repo auxiliary checkpoints in pull snapshot reuse
* test(server): stale provenance snapshot must not flag a false update
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(chat): add server-side thinking control (#3132)
* fix: stop request during prefill enabled (#3133)
* ci: stop building unused targets in the validate workflows (#3052)
* ci: stop building unused targets in the validate workflows
The llama.cpp and vLLM validate jobs built every CMake target; both only
need lemond (plus the CLI for sd.cpp/vLLM). Cache build/_deps in the three
validate jobs and drop the `Remove-Item build` that would have deleted the
restored cache.
- validate_llamacpp: all targets -> lemond, BUILD_WEB_APP=OFF, _deps cache
- validate_sdcpp: _deps cache, drop build/ wipe
- validate_vllm: all targets -> lemond lemonade, _deps cache
- actions/cache v4 -> v5 in the Windows embeddable job
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci: address review on the validate build caches
- vLLM: install build deps directly instead of ./setup.sh, whose
"Preparing build directory" step rm -rf's the restored _deps cache
- share one _deps cache between the two Windows validate jobs rather
than writing one each; the repo is near the 10 GB Actions cache cap
- add toolchain/runner-image markers and cmake/*.cmake to the cache keys
- verify build/lemonade in the vLLM build job
- finish the action-version bump in the Windows embeddable job
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Revert "ci: bump action versions in the Windows embeddable job"
The embeddable job neither builds validate targets nor shares a cache
with them, so the action-version bumps do not belong in this PR.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci: drop the _deps cache from the validate workflows
The warm-cache rerun showed it does not pay for itself: 2 of 3 jobs
missed even though the prior attempt had saved both keys 14 hours
earlier (the repo's Actions cache is at its 10 GB cap, so ~550 MB
entries are evicted within hours), and the job that did hit got slower.
Configure dominates these builds and reconfigures either way, leaving a
~30s ceiling on a 10-minute job.
Also reverts the two changes that only existed to protect the cache:
the `Remove-Item build` deletions and vLLM's direct apt-get install in
place of setup.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(server): harden extra models directory handling (#3105)
* fix(server): harden extra models directory handling
* address review comments
* add missing CMake test addition
---------
Co-authored-by: Slawomir Nowaczyk <slawomir.nowaczyk@amd.com>
Co-authored-by: Michele Balistreri <michele@bitgamma.com>
Co-authored-by: Yiğit ERDOĞAN <yigiterdogan023@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Jeremy Fowers <80718789+jeremyfowers@users.noreply.github.com>
Co-authored-by: Andi Milhomme <Mandirosa3@gmail.com>
Co-authored-by: Andi M <webmaster@anditherobot.com>
Co-authored-by: Chris Kuethe <ckuethe@users.noreply.github.com>
Co-authored-by: Andrew Vavilchenko <47447306+blackdeathdrow@users.noreply.github.com>
Co-authored-by: Ken VanDine <ken@vandine.org>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Salah Eddine Bekhouche <bekhouchesalah@gmail.com>
Co-authored-by: Alan Pope <alan@tessl.io>
Co-authored-by: Arun Babu Neelicattu <arun.neelicattu@gmail.com>
Co-authored-by: Dennis Huang <huangsiyuan20060408@hotmail.com>
Co-authored-by: hogeheer499-commits <hogeheer499@gmail.com>
Co-authored-by: hogeheer <hogeheer@users.noreply.github.com>
Co-authored-by: hogeheer499-commits <hogeheer499-commits@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mario Limonciello <mario.limonciello@amd.com>
Co-authored-by: Ramakrishnan Sivakumar <ramkrishna2910@gmail.com>
Co-authored-by: GiAnG <yelliver@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
POST /v1/messagesanswered HTTP 200 with an empty text block whenever the backend failed, onboth the non-streaming and the streaming path. A caller — Claude Code, say — sees "the model had
nothing to say" instead of the error.
convert_openai_chat_to_anthropic(),which has no
errorbranch and falls into its empty-content path. Every Ollama handler in thesame class guards this with
send_backend_error(); the OpenAI path usesset_error_response().data: {"error": ...}event. The Anthropic adapter inspects onlyid,usageandchoices[0], so it dropped that event and closed the stream withstop_reason: "end_turn".It now emits an Anthropic
event: errorframe and stops there, with no closing message frames.Verified against
llamacpp:vulkan+Tiny-Test-Model-GGUFwith a context-overflow prompt:Both new tests in
test_ollama.pyfail against a build ofmainand pass with this change;test_024/test_025andserver_streaming_errors.pystill pass.One open question:
backend_error_http_status()covers the same ground asget_error_status_code()inserver.cpp, which is file-local. I kept a local helper rather thanwidening that file's API surface, but I'm happy to export it in a header and call it from here
instead — just say which you prefer.