Skip to content

[oci] fix: accept oci:// references in GetImageDigest so install and upgrade record a digest - #12203

Open
walcz-de wants to merge 202 commits into
mudler:masterfrom
walcz-de:fix/oci-digest-record
Open

walcz-de wants to merge 202 commits into
mudler:masterfrom
walcz-de:fix/oci-digest-record

Conversation

@walcz-de

Copy link
Copy Markdown
Contributor

Description

Follow-up to #12138. That change strips the oci:// scheme in the upgrade check, but the two places that record a digest still hand the raw gallery URI to oci.GetImageDigest:

  • core/gallery/backends.go — after an install
  • core/gallery/upgrade.go — after an upgrade

name.ParseReference cannot parse the scheme, so the call fails with could not parse reference: oci://… and no digest is ever written to metadata.json. For a self-hosted gallery (which must use the oci:// scheme to be recognised as OCI) the check then compares the registry digest against nothing and reports "upgrade available (new build)" forever; every click re-installs the same image and the flag comes straight back.

This PR normalises the reference once inside GetImageDigest, so install, upgrade and check all behave the same, and adds a unit test that also asserts the raw oci:// form is unparseable (otherwise the normalisation would be moot).

Observed on LocalAI v4.10.0-263 with a private gallery pointing at oci://registry:5556/…: five backends flagged permanently, log line Failed to get OCI image digest after upgrade … could not parse reference: oci://….

Notes for Reviewers

  • Behaviour for plain references is unchanged (TrimPrefix is a no-op there).
  • Tested with go test ./pkg/oci/ and go vet ./pkg/oci/; a full make build of the tree runs in our nightly.

Signed commits

  • Yes, I signed my commits.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PHcZ8CTpZEoK3qgroNnkB4

walcz-de added 30 commits May 7, 2026 10:15
Adds pkg/tokens/ wrapping pkoukk/tiktoken-go with three exported functions
(Count, CountText, EncodingFor) and a small alias table. Promotes tiktoken-go
from indirect to direct dependency.

Counts are exact for OpenAI families (cl100k_base, o200k_base) and approximate
(within ~5%) for other model families — accurate enough for compression-trigger
thresholds and cost prediction, not for billing.

Implements RFC mudler#9537. Does not change behavior of any existing endpoint;
purely additive utility code under MIT.

References: mudler#9537

Assisted-by: Claude:claude-opus-4-7
Foundation for the upcoming context-compression middleware (RFC mudler#9534).
This commit adds two pieces:

1. CompressionConfig in core/config/model_config.go — per-model YAML
   schema mirroring FunctionsConfig / MCPConfig style. Six fields:
   enabled, trigger_at_ratio, keep_tail_tokens, max_summary_tokens,
   compressor_model, on_post_compression_overflow. Opt-in, default off,
   absent block changes nothing.

2. pkg/compress/ — algorithmic core, decoupled from HTTP layer:
   - Compress entrypoint with Summarizer interface
   - partition() splits messages into a head (compress) and tail (keep)
     by token count, preserving keep_tail_tokens
   - drop-oldest-summary overflow recovery (default) and error mode
   - 14 Ginkgo specs covering passthrough, compression, model fallback,
     summarizer error propagation, and overflow recovery

The middleware that wires this into /v1/chat/completions follows in a
subsequent commit.

References: mudler#9534

Assisted-by: Claude:claude-opus-4-7
Adds the HTTP-layer glue for the compression feature introduced in the
previous commit:

* core/http/middleware/compression.go — Echo middleware that runs after
  SetOpenAIRequest, reads the parsed *schema.OpenAIRequest and
  *config.ModelConfig from context, and calls pkg/compress.Compress with
  a backend-backed Summarizer when CompressionConfig.Enabled is set.
  Compression failures fall through to passthrough rather than aborting
  the request — best-effort optimisation must never break the underlying
  chat completion.

* CompressionMiddlewareWithSummarizer is exported as a test seam so
  Ginkgo specs can inject a stub Summarizer without standing up an
  Application.

* The Summarizer implementation invokes the configured compressor model
  via core/backend.ModelInferenceFunc with a fixed compression prompt;
  empty predInput relies on the model's chat template (operators must
  set use_jinja: true on the compressor model).

* Registers the middleware on /v1/chat/completions (routes/openai.go)
  and /v1/mcp/chat/completions (routes/localai.go).

* 10 Ginkgo specs cover passthrough, compression-applied path,
  summarizer-error fallthrough, and default-injection behaviour.

Sensible defaults applied when only `enabled: true` is set:
trigger_at_ratio=0.75, keep_tail_tokens=8000, max_summary_tokens=2048,
on_post_compression_overflow=drop_oldest_summary.

Compression result is attached to the Echo context via
CONTEXT_LOCALS_KEY_COMPRESSION_RESULT so a future commit can surface
usage.compression_meta in the response body.

References: mudler#9534

Assisted-by: Claude:claude-opus-4-7
User-facing documentation for the optional context-compression feature
introduced in the previous two commits.

Covers:
- when to enable
- full YAML field reference with defaults
- compressor-model requirements (use_jinja: true)
- how the middleware partitions and summarises
- best-effort failure semantics
- backward compatibility (additive, opt-in)
- known limitations (approximate counts, single-pass, no cross-request memory)
- troubleshooting common scenarios

References: mudler#9534

Assisted-by: Claude:claude-opus-4-7
Operator-level kill-switch that disables the context-compression
middleware globally, overriding any per-model `compression.enabled: true`
settings without requiring YAML edits. Mirrors the pattern of
LOCALAI_DISABLE_AGENTS / LOCALAI_DISABLE_MCP.

When set, the middleware is replaced with a passthrough at chain
construction time, so the kill-switch has zero per-request overhead.

References: mudler#9534

Assisted-by: Claude:claude-opus-4-7
…on-middleware

Conflicts (resolved additively — both sides are independent feature
adds, neither overrides the other):

- core/config/model_config.go: keep our Compression field + type AND
  upstream's new PII/Router/Proxy/MITM/Limits fields and types.

- core/http/routes/openai.go (chat middleware chain): keep upstream's
  RouteModel → AdmissionControl → PII redaction chain, with our
  CompressionMiddleware appended as the innermost step. Order rationale:
  RouteModel resolves the served model first, AdmissionControl gates on
  the served model's limits, PII redaction runs before any LLM (including
  the compression summariser) sees content, Compression operates last on
  the PII-redacted request using the served model's CompressionConfig.
# Conflicts:
#	core/config/model_config.go
#	core/http/routes/openai.go
walcz-de and others added 30 commits September 4, 2026 15:49
The fork sat on llama.cpp 3466812d while the llama-cpp backend builds 9cffdcc.
That is 22 commits apart, and it made every cross-backend comparison ambiguous:
an agent-path measurement of an FP4 model against a normal one was measuring two
llama.cpp vintages as much as two quantizations.

The eight fork commits cherry-pick onto 9cffdcc without a single conflict, and
the only MMQ-area movement in those 22 commits is 16 deleted lines
(mmq-vec-dot.cuh -11, mmq.cuh -5) — nothing the FP4 kernels had to be
re-expressed against.

Branch walcz-de/llama.cpp-ROCmFP4:rocmfp4-rebase-9cffdcc, head 32284d40b7ddac920b06c4587e50c01a3bf38ac1.
The rocmfp4 branch and the pin the open PR carries are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PHcZ8CTpZEoK3qgroNnkB4
…ate system turns

Tokenizer chat templates such as Qwen3.8 / Qwen3.8-Flash-Next raise
'System message must be at the beginning' for system-role messages that
appear after the leading system block, while agent frameworks (cogito tool
selection and adjustment prompts) legitimately append system instructions
mid-conversation. Every such request failed with a 500 (48 errors in one
10-task agent run).

New per-model option template.system_messages_after_first:
  merge  fold late system turns into the leading system message
  user   forward them as user-role turns at their original position
Default (unset) keeps the current pass-through behaviour.

Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
…Finish idempotent (double finish panicked the agent)

Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
…ot/cache reuse after parallel burst on gfx1151 with 67672dc5 (nonce probe red 3/5); revert patch c7d87229 alone no longer sufficient; upstream report pending

Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
github.com/mudler/localrecall v0.6.3 destroys the hybrid-search ranking: the RRF fusion
divides the weight parameters by a bigint rank, so PostgreSQL infers them as bigint and
the division is integer division — every weight below 61 yields a score of exactly 0.
All candidates tie, ORDER BY similarity DESC orders by nothing, and the collection search
endpoint returns the join order instead of a ranking. HYBRID_SEARCH_BM25_WEIGHT and
HYBRID_SEARCH_VECTOR_WEIGHT therefore never had any effect.

Measured on the live tax-law collection (650 chunks, four questions with a verifiable
answer), correct document in the top three: 2/4 before, 4/4 after. A verbatim excerpt of a
stored document did not retrieve its own chunk at rank 1, while a direct pgvector KNN on
the same query was correct — embeddings and index were healthy all along.

Replace points at walcz-de/LocalRecall branch fix/rrf-integer-division (also carries the
BM25 text_config knob from 2026-07 and the deferred search_vector follow-up). Upstream PR
against mudler/localrecall still to be opened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	backend/cpp/llama-cpp/Makefile
# Conflicts:
#	Makefile
#	docs/content/features/backends.md
…zt verlangt

Der Upstream-Merge brachte LocalAI-Code, der eine neuere LocalAGI-Schnittstelle ruft
(state.PoolLimits, conversations.RetentionPolicy, scheduler.RetentionPolicy/CreationPolicy,
state.SkillsProvider). Unsere `replace`-Ersetzung hielt den Fork-Stand vom 2026-09-08 fest, der diese
Typen nicht kennt — `make build` brach in core/services/agentpool ab.

Das ist die in CLAUDE.md beschriebene Kopplung, nur andersherum als dort notiert: nicht wir bumpen
eigenmaechtig, sondern upstream zieht seinen Pin (jetzt d93d478e42f1) und unser Override bleibt
zurueck. Abstand waren 20 Commits.

Unsere sechs Commits sind konfliktfrei auf d93d478e42f1 aufgesetzt und leben unveraendert weiter:
Selbstkorrektur bei unbekannten Werkzeugen, Grounding-Tor (zwei Stufen), Reihenfolge im
Nachrichtenstrom, Idempotenz von JobResult.Finish, 10-Minuten-Rueckfall bei unlesbarer Laufzeit.
Neuer Fork-Zweig fix/walcz-patches-on-d93d478 (716a840) statt Force-Update des alten — so muss die
neue Push-Sperre nicht uebergangen werden, und die alte Linie bleibt als Beleg stehen.

Geprueft: core/ des Forks uebersetzt, core/types gruen, core/agent laeuft in die Frist — auf dem
REINEN upstream-Stand d93d478 ebenso, also vorbestehend (die Suite braucht eine laufende LocalAI).
core/services/agentpool von LocalAI uebersetzt mit der neuen Ersetzung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paradigma (Stefan 2026-09-12): Upstream ist der Standard, eine abweichende Pin nur wenn upstream
BEWIESEN kaputt ist. Fuer 465e49b9 gab es diesen Beweis nie — der eigene Bisect-Plan weist ihn als
offen aus: die schlechte Evidenz war EIN warmer Prozess, der 3 Proben in 5 Minuten fehlschlug,
waehrend dasselbe Binaer davor 7x sauber war, und die in §1 verlangte Baseline wurde nie gefahren.
Ohne Beweis geht der Pin nach oben: 465e49b9 -> df03399b.

Damit fallen zwei Patches ERSATZLOS weg, weil upstream sie selbst uebernommen hat:
  · backend/cpp/llama-cpp/patches/0001-revert-c7d87229-hip-integrated-crossover.patch
  · backend/cpp/rocmfp4/patches/0001-revert-c7d87229-hip-integrated-crossover.patch
Upstream d4389a4dd revertiert c7d87229 selbst (#28604), enthalten ab df03399b. Dazu kommt #28326
(iGPU-Lazy-Loading aus) und #28302 (Checkpoint-Verdraengung) — dieselbe Fehlerklasse, oben behandelt.

Die uebrigen zwei llama-cpp-Patches (score, tts) wurden am neuen Pin geprueft und werden weiter
gebraucht — in der Reihenfolge, in der prepare.sh sie faehrt: tts setzt auf score auf und meldet
einzeln geprueft faelschlich "passt nicht". Genau dieser Messfehler steckte auch im Tor und ist
behoben; ebenso, dass es Patches fremder Backends gegen den llama.cpp-Pin geprueft hat.

Register: von 10 Abweichungen auf 7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The backend upgrade check gates on `downloader.URI(...).LooksLikeOCI()` and then passes the
gallery URI unchanged to `oci.GetImageDigest`, which calls `name.ParseReference`. That parser
does not accept an `oci://` scheme, so the lookup fails with "could not parse reference" and
the upgrade is skipped — visible only as a WARN at startup.

LooksLikeOCI() recognises an image either by a fixed host list (quay.io, ghcr.io, docker.io)
or by the explicit oci:// scheme. Users pulling backends from those three hosts write no
scheme and never hit this. Users running their own registry MUST set the scheme to be
recognised at all — and therefore lose backend upgrade detection entirely, silently.

Observed on a self-hosted gallery, every start, for all four locally built backends:

    WARN Failed to get remote OCI digest for upgrade check backend="..."
         error=could not parse reference: oci://registry.example:5556/localai-backends:...-vllm

while gallery backends from quay.io report upgrades normally in the same run.

Dropping the scheme from the gallery entry is not a workaround: without it LooksLikeOCI()
no longer treats the registry as an image source and the backends cannot be installed.

Stripping happens at the call site because pkg/downloader imports pkg/oci — the constant
cannot be referenced from inside GetImageDigest without an import cycle. This matches the
two existing call sites that strip the prefix themselves (pkg/downloader/uri.go,
core/gallery/importers/llama-cpp.go).

Verified: `go build ./core/gallery/...` and `go test ./core/gallery/` pass.
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
ROCMFP4_VERSION 32284d40 -> 5f91425d. Der Fork stand 216 Commits und 13 Tage hinter
LLAMA_VERSION; das Wrapper-Makefile loescht vor dem Bauen die fuer upstream-llama.cpp
vendorierten Patches und setzt damit voraus, dass der Fork auf genau diesem Pin sitzt.

Der Rebase war kein Rebase, sondern ein Port: upstream 5a4d0fecae hat den
FlashAttention-Vec-Dispatch ersetzt. Statt handgepflegter Instanzliste plus Makro-Kaskade
gibt es jetzt einen CMake-Generator (ggml_cuda_fattn_vec_instances) und eine Registry
(ggml_cuda_get_fattn_vec_case), die ueber generierte GGML_CUDA_FA_<K>_<V>-Makros
entscheidet, was kompiliert wird.

Unsere FP4-Typen sind deshalb dorthin gewandert, wo upstream sie erwartet:
  ggml/cmake/common.cmake  FA_TYPES       += q4_0_rocmfp4 q4_0_rocmfp4_fast
                                             q3_0_rocmfpx q6_0_rocmfpx q8_0_rocmfpx
  ggml/CMakeLists.txt      FA_QUANTS-Vorgabe += die fuenf symmetrischen Paare
  ggml/src/ggml-cuda/fattn.cu  Registry-Zeilen + kv_type_supported

Zwei Dinge sind dabei WENIGER geworden, und das ist der Punkt der Uebung:
  · "register the FA instances with the HIP backend's own source list" ist ERSATZLOS
    entfallen — upstream ruft den Generator jetzt selbst in ggml-hip/CMakeLists.txt auf
    und liest dieselbe Auswahl. Sieben Commits statt acht.
  · Die TurboQuant-Paare habe ich beim Portieren zuerst mitgenommen und dann wieder
    entfernt: der naechste Commit der Serie verwirft sie ausdruecklich (Linkfehler,
    "dieses Backend bedient ROCmFP4-GEWICHTE"). Sie stehen jetzt weder in FA_TYPES noch
    in der Auswahl noch in der Registry; die TYPEN bleiben deklariert, damit die
    ggml-Typnummern zu veroeffentlichten GGUFs passen.

Gegengeprueft: jede Kombination der FA_QUANTS-Vorgabe hat ihre Instanzdatei (11/11) —
sonst bricht der neue Generator hart ab. GGML_CUDA_FA_QUANTS="all" ist auf diesem Fork
NICHT benutzbar (196 Kombinationen, nur symmetrische Instanzen); das ist im common.cmake
vermerkt.

Fork-Zweig: walcz-de/llama.cpp-ROCmFP4 rocmfp4-rebase-38a5b42d

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1HR5SwH4eT33fELQ2yvHG
…pgrade record a digest

mudler#12138 stripped the oci:// scheme in the upgrade check, but the two paths that
record the digest after an install (core/gallery/backends.go) and after an upgrade
(core/gallery/upgrade.go) still hand the raw gallery URI to GetImageDigest, which
fails with "could not parse reference: oci://…". No digest is ever stored for a
self-hosted gallery, so the check compares the registry digest against nothing and
reports "upgrade available (new build)" forever; every click re-installs the same
image and the flag comes back.

Normalise once inside GetImageDigest so every caller behaves the same, and cover
it with a unit test that also asserts the raw oci:// form is unparseable.

Observed on LocalAI v4.10.0-263 with an oci:// gallery on a private registry:
five backends flagged permanently, log "Failed to get OCI image digest after
upgrade … could not parse reference: oci://registry0…:5556/…".

Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
Assisted-by: Claude:claude-fable-5-1

This branch has not been deployed

No deployments
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.

1 participant