Compare commits

...

13 Commits

Author SHA1 Message Date
Ettore Di Giacinto
ff2ce906c8 fix(auth): accept EC/PS/EdDSA-signed OIDC ID tokens, not just RS256
The OIDC verifier was built with a bare oidc.Config{ClientID: ...}, so
go-oidc applied its default of accepting RS256-signed ID tokens only. An
identity provider configured with an EC signing key (e.g. Authentik) issues
ES256-signed tokens, and the callback failed verification with:

  failed to verify ID token: oidc: malformed jwt: unexpected signature
  algorithm "HS256"; expected ["RS256"]

surfacing to the user as HTTP 500 "failed to fetch user info" (#10677; the
underlying cause became visible after the logging fix in #10679).

Set SupportedSigningAlgs to the standard asymmetric algorithms
(RS256/384/512, ES256/384/512, PS256/384/512, EdDSA). All are verified
against the provider's published JWKS. HS256 is intentionally excluded: it
is symmetric and would validate against the client secret, a different and
security-sensitive trust model.

Tested with a functional spec that signs an ES256 ID token and confirms it
verifies with the configured algorithms and is rejected under go-oidc's
RS256-only default (using oidc.StaticKeySet, no network).

Closes #10677

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
2026-07-07 21:31:09 +00:00
Tai An
2f33cc7bc4 fix(vram): report largest GGUF quant instead of whole HF repo for gallery size (#10700) (#10707)
* fix(vram): report largest GGUF quant, not whole repo, for HF gallery size (#10700)

Signed-off-by: Tai An <antai12232931@outlook.com>

* test(vram): cover multi-GGUF quant repo size estimation (#10700)

Signed-off-by: Tai An <antai12232931@outlook.com>

---------

Signed-off-by: Tai An <antai12232931@outlook.com>
Co-authored-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-07-07 11:50:27 +00:00
LocalAI [bot]
22225217e0 docs: ⬆️ update docs version mudler/LocalAI (#10709)
⬆️ Update docs version mudler/LocalAI

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-07-07 08:55:01 +02:00
LocalAI [bot]
c1fd12a506 chore: ⬆️ Update ggml-org/llama.cpp to f36e5c348bc8795c34f9a038e58876e7a8423d4d (#10710)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-07-07 08:54:33 +02:00
LocalAI [bot]
d01b2c4f46 chore: ⬆️ Update ServeurpersoCom/qwentts.cpp to 0725d2e53b7d2749a99ff33d3a460b954ffa7805 (#10711)
⬆️ Update ServeurpersoCom/qwentts.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-07-07 08:41:46 +02:00
LocalAI [bot]
fb9ff061f1 chore: ⬆️ Update vllm-metal (darwin) to v0.3.0.dev20260706123649 (#10712)
⬆️ Update vllm-project/vllm-metal (darwin)

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-07-07 08:41:34 +02:00
LocalAI [bot]
40f847745e chore: ⬆️ Update ikawrakow/ik_llama.cpp to a8cf53fd69bada5450bd653eb0e32d1113fbd7fe (#10713)
⬆️ Update ikawrakow/ik_llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-07-07 08:41:21 +02:00
LocalAI [bot]
ba9327b9f8 chore: ⬆️ Update CrispStrobe/CrispASR to 0a7643f1006c6bf2d1f37f5c63e9726f5eb4f364 (#10716)
⬆️ Update CrispStrobe/CrispASR

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-07-07 08:41:00 +02:00
LocalAI [bot]
fd467c5b3b chore: ⬆️ Update leejet/stable-diffusion.cpp to bb84971129d2a094ab8051c6feed5406d3b4409d (#10684)
* ⬆️ Update leejet/stable-diffusion.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(stablediffusion-ggml): pass chroma knobs via model_args after upstream API change

Upstream stable-diffusion.cpp bb849711 removed the dedicated
chroma_use_dit_mask / chroma_use_t5_mask / chroma_t5_mask_pad fields from
sd_ctx_params_t and now reads them from the generic model_args key=value
spec (parse_key_value_args). Assigning the old struct members broke the
gosd.cpp build. Emit the three options into model_args instead so the
existing chroma controls keep working. Verified by building
libgosd-fallback.so against the pinned upstream commit.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-07-06 23:57:50 +00:00
dependabot[bot]
fa0622604a chore(deps): bump actions/cache from 4 to 6 (#10704)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 21:39:37 +02:00
Dennis Huang
ff5758113b chore(model gallery): add MiniCPM series models (#10699)
Add 9 MiniCPM models to the gallery:
- MiniCPM-V 4.6 (1.3B multimodal, edge-optimized)
- MiniCPM-V 4.6 Thinking (1.3B multimodal with reasoning)
- MiniCPM-V 4 (multimodal)
- MiniCPM-o 4.5 (8B omni-modal, vision+speech)
- MiniCPM-o 2.6 (7.6B omni-modal)
- MiniCPM5-1B (text)
- MiniCPM4.1-8B (text)
- MiniCPM4-8B (text)
- MiniCPM3-4B (text)

All sha256 checksums sourced from HuggingFace LFS metadata.

Signed-off-by: Dennis Huang <huangsiyuan20060408@hotmail.com>
2026-07-06 21:39:12 +02:00
LocalAI [bot]
29db4ab414 fix(ci): shard single-arch backend matrix under GitHub's 256-job limit (#10703)
GitHub Actions refuses to instantiate a matrix that would generate more
than 256 jobs. It does so silently: the job hangs forever at "Waiting for
pending jobs" and the whole run is marked `failure` while every other job
stays green. This is exactly what happened on the v4.6.1 tag build
(run 28786533892): the single-arch build matrix had grown to 268 entries,
so `backend-jobs-singlearch` (and its downstream merge) never produced a
single job, and the release build "failed" with no failing job to point at.

The single-arch list is the one that grows unbounded as backends are added,
so shard it across a fixed number of matrix jobs (SINGLEARCH_SHARDS=4,
~67 entries each today, headroom to ~1020 backends). Each merge shard
`needs:` only its matching build shard, preserving the "merge waits only on
its own build" property that keeps slow CUDA/ROCm builds from gating
multi-arch manifest assembly.

changed-backends.js now emits per-shard matrix/has-* outputs and throws
loudly if a shard ever reaches the 256 limit (telling the maintainer to
bump SINGLEARCH_SHARDS and add matching job blocks) instead of letting
GitHub drop the overflow silently. backend.yml and backend_pr.yml define
the four build + four merge shard jobs; multi-arch and darwin groups are
untouched.


Assisted-by: Claude:claude-opus-4-8 [Claude Code]

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-07-06 21:38:12 +02:00
weifanglab
a6cf67cc6b refactor: use slices.Contains to simplify code (#10702)
Signed-off-by: weifanglab <weifanglab@outlook.com>
2026-07-06 19:33:28 +02:00
19 changed files with 852 additions and 74 deletions

View File

@@ -32,16 +32,30 @@ jobs:
if: github.repository == 'mudler/LocalAI'
runs-on: ubuntu-latest
outputs:
matrix-singlearch: ${{ steps.set-matrix.outputs['matrix-singlearch'] }}
matrix-multiarch: ${{ steps.set-matrix.outputs['matrix-multiarch'] }}
matrix-darwin: ${{ steps.set-matrix.outputs['matrix-darwin'] }}
merge-matrix-multiarch: ${{ steps.set-matrix.outputs['merge-matrix-multiarch'] }}
merge-matrix-singlearch: ${{ steps.set-matrix.outputs['merge-matrix-singlearch'] }}
has-backends-singlearch: ${{ steps.set-matrix.outputs['has-backends-singlearch'] }}
has-backends-multiarch: ${{ steps.set-matrix.outputs['has-backends-multiarch'] }}
has-backends-darwin: ${{ steps.set-matrix.outputs['has-backends-darwin'] }}
has-merges-multiarch: ${{ steps.set-matrix.outputs['has-merges-multiarch'] }}
has-merges-singlearch: ${{ steps.set-matrix.outputs['has-merges-singlearch'] }}
# Single-arch backends are sharded across SINGLEARCH_SHARDS matrix jobs to
# stay under GitHub's 256-jobs-per-matrix limit (see changed-backends.js).
matrix-singlearch-1: ${{ steps.set-matrix.outputs['matrix-singlearch-1'] }}
merge-matrix-singlearch-1: ${{ steps.set-matrix.outputs['merge-matrix-singlearch-1'] }}
has-backends-singlearch-1: ${{ steps.set-matrix.outputs['has-backends-singlearch-1'] }}
has-merges-singlearch-1: ${{ steps.set-matrix.outputs['has-merges-singlearch-1'] }}
matrix-singlearch-2: ${{ steps.set-matrix.outputs['matrix-singlearch-2'] }}
merge-matrix-singlearch-2: ${{ steps.set-matrix.outputs['merge-matrix-singlearch-2'] }}
has-backends-singlearch-2: ${{ steps.set-matrix.outputs['has-backends-singlearch-2'] }}
has-merges-singlearch-2: ${{ steps.set-matrix.outputs['has-merges-singlearch-2'] }}
matrix-singlearch-3: ${{ steps.set-matrix.outputs['matrix-singlearch-3'] }}
merge-matrix-singlearch-3: ${{ steps.set-matrix.outputs['merge-matrix-singlearch-3'] }}
has-backends-singlearch-3: ${{ steps.set-matrix.outputs['has-backends-singlearch-3'] }}
has-merges-singlearch-3: ${{ steps.set-matrix.outputs['has-merges-singlearch-3'] }}
matrix-singlearch-4: ${{ steps.set-matrix.outputs['matrix-singlearch-4'] }}
merge-matrix-singlearch-4: ${{ steps.set-matrix.outputs['merge-matrix-singlearch-4'] }}
has-backends-singlearch-4: ${{ steps.set-matrix.outputs['has-backends-singlearch-4'] }}
has-merges-singlearch-4: ${{ steps.set-matrix.outputs['has-merges-singlearch-4'] }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
@@ -109,9 +123,9 @@ jobs:
# take their full ~6h cold without blocking manifest assembly for the
# multi-arch backends whose per-arch digests would otherwise sit untagged
# on quay long enough to be GC'd.
backend-jobs-singlearch:
backend-jobs-singlearch-1:
needs: generate-matrix
if: needs.generate-matrix.outputs['has-backends-singlearch'] == 'true'
if: needs.generate-matrix.outputs['has-backends-singlearch-1'] == 'true'
uses: ./.github/workflows/backend_build.yml
with:
tag-latest: ${{ matrix.tag-latest }}
@@ -138,7 +152,100 @@ jobs:
strategy:
fail-fast: false
max-parallel: 8
matrix: ${{ fromJson(needs.generate-matrix.outputs['matrix-singlearch']) }}
matrix: ${{ fromJson(needs.generate-matrix.outputs['matrix-singlearch-1']) }}
backend-jobs-singlearch-2:
needs: generate-matrix
if: needs.generate-matrix.outputs['has-backends-singlearch-2'] == 'true'
uses: ./.github/workflows/backend_build.yml
with:
tag-latest: ${{ matrix.tag-latest }}
tag-suffix: ${{ matrix.tag-suffix }}
build-type: ${{ matrix.build-type }}
cuda-major-version: ${{ matrix.cuda-major-version }}
cuda-minor-version: ${{ matrix.cuda-minor-version }}
platforms: ${{ matrix.platforms }}
platform-tag: ${{ matrix.platform-tag || '' }}
runs-on: ${{ matrix.runs-on }}
builder-base-image: ${{ matrix.builder-base-image || '' }}
base-image: ${{ matrix.base-image }}
backend: ${{ matrix.backend }}
dockerfile: ${{ matrix.dockerfile }}
skip-drivers: ${{ matrix.skip-drivers }}
context: ${{ matrix.context }}
ubuntu-version: ${{ matrix.ubuntu-version }}
amdgpu-targets: ${{ matrix.amdgpu-targets || 'gfx908,gfx90a,gfx942,gfx950,gfx1030,gfx1100,gfx1101,gfx1102,gfx1151,gfx1200,gfx1201' }}
secrets:
dockerUsername: ${{ secrets.DOCKERHUB_USERNAME }}
dockerPassword: ${{ secrets.DOCKERHUB_PASSWORD }}
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: false
max-parallel: 8
matrix: ${{ fromJson(needs.generate-matrix.outputs['matrix-singlearch-2']) }}
backend-jobs-singlearch-3:
needs: generate-matrix
if: needs.generate-matrix.outputs['has-backends-singlearch-3'] == 'true'
uses: ./.github/workflows/backend_build.yml
with:
tag-latest: ${{ matrix.tag-latest }}
tag-suffix: ${{ matrix.tag-suffix }}
build-type: ${{ matrix.build-type }}
cuda-major-version: ${{ matrix.cuda-major-version }}
cuda-minor-version: ${{ matrix.cuda-minor-version }}
platforms: ${{ matrix.platforms }}
platform-tag: ${{ matrix.platform-tag || '' }}
runs-on: ${{ matrix.runs-on }}
builder-base-image: ${{ matrix.builder-base-image || '' }}
base-image: ${{ matrix.base-image }}
backend: ${{ matrix.backend }}
dockerfile: ${{ matrix.dockerfile }}
skip-drivers: ${{ matrix.skip-drivers }}
context: ${{ matrix.context }}
ubuntu-version: ${{ matrix.ubuntu-version }}
amdgpu-targets: ${{ matrix.amdgpu-targets || 'gfx908,gfx90a,gfx942,gfx950,gfx1030,gfx1100,gfx1101,gfx1102,gfx1151,gfx1200,gfx1201' }}
secrets:
dockerUsername: ${{ secrets.DOCKERHUB_USERNAME }}
dockerPassword: ${{ secrets.DOCKERHUB_PASSWORD }}
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: false
max-parallel: 8
matrix: ${{ fromJson(needs.generate-matrix.outputs['matrix-singlearch-3']) }}
backend-jobs-singlearch-4:
needs: generate-matrix
if: needs.generate-matrix.outputs['has-backends-singlearch-4'] == 'true'
uses: ./.github/workflows/backend_build.yml
with:
tag-latest: ${{ matrix.tag-latest }}
tag-suffix: ${{ matrix.tag-suffix }}
build-type: ${{ matrix.build-type }}
cuda-major-version: ${{ matrix.cuda-major-version }}
cuda-minor-version: ${{ matrix.cuda-minor-version }}
platforms: ${{ matrix.platforms }}
platform-tag: ${{ matrix.platform-tag || '' }}
runs-on: ${{ matrix.runs-on }}
builder-base-image: ${{ matrix.builder-base-image || '' }}
base-image: ${{ matrix.base-image }}
backend: ${{ matrix.backend }}
dockerfile: ${{ matrix.dockerfile }}
skip-drivers: ${{ matrix.skip-drivers }}
context: ${{ matrix.context }}
ubuntu-version: ${{ matrix.ubuntu-version }}
amdgpu-targets: ${{ matrix.amdgpu-targets || 'gfx908,gfx90a,gfx942,gfx950,gfx1030,gfx1100,gfx1101,gfx1102,gfx1151,gfx1200,gfx1201' }}
secrets:
dockerUsername: ${{ secrets.DOCKERHUB_USERNAME }}
dockerPassword: ${{ secrets.DOCKERHUB_PASSWORD }}
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: false
max-parallel: 8
matrix: ${{ fromJson(needs.generate-matrix.outputs['matrix-singlearch-4']) }}
# Apply tags to per-arch digests via `imagetools create`. Split into two
# jobs that mirror the build split so each merge waits ONLY on its
@@ -174,10 +281,12 @@ jobs:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs['merge-matrix-multiarch']) }}
backend-merge-jobs-singlearch:
needs: [generate-matrix, backend-jobs-singlearch]
# See note on backend-merge-jobs-multiarch above for !cancelled().
if: ${{ !cancelled() && needs.generate-matrix.outputs['has-merges-singlearch'] == 'true' }}
# One merge shard per build shard: backend-merge-jobs-singlearch-<n> needs only
# backend-jobs-singlearch-<n>, preserving the "merge waits only on its own
# build" property while staying under the 256-jobs-per-matrix limit.
backend-merge-jobs-singlearch-1:
needs: [generate-matrix, backend-jobs-singlearch-1]
if: ${{ !cancelled() && needs.generate-matrix.outputs['has-merges-singlearch-1'] == 'true' }}
uses: ./.github/workflows/backend_merge.yml
with:
tag-latest: ${{ matrix.tag-latest }}
@@ -189,7 +298,55 @@ jobs:
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs['merge-matrix-singlearch']) }}
matrix: ${{ fromJson(needs.generate-matrix.outputs['merge-matrix-singlearch-1']) }}
backend-merge-jobs-singlearch-2:
needs: [generate-matrix, backend-jobs-singlearch-2]
if: ${{ !cancelled() && needs.generate-matrix.outputs['has-merges-singlearch-2'] == 'true' }}
uses: ./.github/workflows/backend_merge.yml
with:
tag-latest: ${{ matrix.tag-latest }}
tag-suffix: ${{ matrix.tag-suffix }}
secrets:
dockerUsername: ${{ secrets.DOCKERHUB_USERNAME }}
dockerPassword: ${{ secrets.DOCKERHUB_PASSWORD }}
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs['merge-matrix-singlearch-2']) }}
backend-merge-jobs-singlearch-3:
needs: [generate-matrix, backend-jobs-singlearch-3]
if: ${{ !cancelled() && needs.generate-matrix.outputs['has-merges-singlearch-3'] == 'true' }}
uses: ./.github/workflows/backend_merge.yml
with:
tag-latest: ${{ matrix.tag-latest }}
tag-suffix: ${{ matrix.tag-suffix }}
secrets:
dockerUsername: ${{ secrets.DOCKERHUB_USERNAME }}
dockerPassword: ${{ secrets.DOCKERHUB_PASSWORD }}
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs['merge-matrix-singlearch-3']) }}
backend-merge-jobs-singlearch-4:
needs: [generate-matrix, backend-jobs-singlearch-4]
if: ${{ !cancelled() && needs.generate-matrix.outputs['has-merges-singlearch-4'] == 'true' }}
uses: ./.github/workflows/backend_merge.yml
with:
tag-latest: ${{ matrix.tag-latest }}
tag-suffix: ${{ matrix.tag-suffix }}
secrets:
dockerUsername: ${{ secrets.DOCKERHUB_USERNAME }}
dockerPassword: ${{ secrets.DOCKERHUB_PASSWORD }}
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs['merge-matrix-singlearch-4']) }}
backend-jobs-darwin:
needs: generate-matrix

View File

@@ -11,16 +11,30 @@ jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
matrix-singlearch: ${{ steps.set-matrix.outputs['matrix-singlearch'] }}
matrix-multiarch: ${{ steps.set-matrix.outputs['matrix-multiarch'] }}
matrix-darwin: ${{ steps.set-matrix.outputs['matrix-darwin'] }}
merge-matrix-multiarch: ${{ steps.set-matrix.outputs['merge-matrix-multiarch'] }}
merge-matrix-singlearch: ${{ steps.set-matrix.outputs['merge-matrix-singlearch'] }}
has-backends-singlearch: ${{ steps.set-matrix.outputs['has-backends-singlearch'] }}
has-backends-multiarch: ${{ steps.set-matrix.outputs['has-backends-multiarch'] }}
has-backends-darwin: ${{ steps.set-matrix.outputs['has-backends-darwin'] }}
has-merges-multiarch: ${{ steps.set-matrix.outputs['has-merges-multiarch'] }}
has-merges-singlearch: ${{ steps.set-matrix.outputs['has-merges-singlearch'] }}
# Single-arch backends are sharded across SINGLEARCH_SHARDS matrix jobs to
# stay under GitHub's 256-jobs-per-matrix limit (see changed-backends.js).
matrix-singlearch-1: ${{ steps.set-matrix.outputs['matrix-singlearch-1'] }}
merge-matrix-singlearch-1: ${{ steps.set-matrix.outputs['merge-matrix-singlearch-1'] }}
has-backends-singlearch-1: ${{ steps.set-matrix.outputs['has-backends-singlearch-1'] }}
has-merges-singlearch-1: ${{ steps.set-matrix.outputs['has-merges-singlearch-1'] }}
matrix-singlearch-2: ${{ steps.set-matrix.outputs['matrix-singlearch-2'] }}
merge-matrix-singlearch-2: ${{ steps.set-matrix.outputs['merge-matrix-singlearch-2'] }}
has-backends-singlearch-2: ${{ steps.set-matrix.outputs['has-backends-singlearch-2'] }}
has-merges-singlearch-2: ${{ steps.set-matrix.outputs['has-merges-singlearch-2'] }}
matrix-singlearch-3: ${{ steps.set-matrix.outputs['matrix-singlearch-3'] }}
merge-matrix-singlearch-3: ${{ steps.set-matrix.outputs['merge-matrix-singlearch-3'] }}
has-backends-singlearch-3: ${{ steps.set-matrix.outputs['has-backends-singlearch-3'] }}
has-merges-singlearch-3: ${{ steps.set-matrix.outputs['has-merges-singlearch-3'] }}
matrix-singlearch-4: ${{ steps.set-matrix.outputs['matrix-singlearch-4'] }}
merge-matrix-singlearch-4: ${{ steps.set-matrix.outputs['merge-matrix-singlearch-4'] }}
has-backends-singlearch-4: ${{ steps.set-matrix.outputs['has-backends-singlearch-4'] }}
has-merges-singlearch-4: ${{ steps.set-matrix.outputs['has-merges-singlearch-4'] }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
@@ -71,10 +85,10 @@ jobs:
fail-fast: true
max-parallel: 8
matrix: ${{ fromJson(needs.generate-matrix.outputs['matrix-multiarch']) }}
backend-jobs-singlearch:
backend-jobs-singlearch-1:
needs: generate-matrix
if: needs.generate-matrix.outputs['has-backends-singlearch-1'] == 'true'
uses: ./.github/workflows/backend_build.yml
if: needs.generate-matrix.outputs['has-backends-singlearch'] == 'true'
with:
tag-latest: ${{ matrix.tag-latest }}
tag-suffix: ${{ matrix.tag-suffix }}
@@ -98,7 +112,94 @@ jobs:
strategy:
fail-fast: true
max-parallel: 8
matrix: ${{ fromJson(needs.generate-matrix.outputs['matrix-singlearch']) }}
matrix: ${{ fromJson(needs.generate-matrix.outputs['matrix-singlearch-1']) }}
backend-jobs-singlearch-2:
needs: generate-matrix
if: needs.generate-matrix.outputs['has-backends-singlearch-2'] == 'true'
uses: ./.github/workflows/backend_build.yml
with:
tag-latest: ${{ matrix.tag-latest }}
tag-suffix: ${{ matrix.tag-suffix }}
build-type: ${{ matrix.build-type }}
cuda-major-version: ${{ matrix.cuda-major-version }}
cuda-minor-version: ${{ matrix.cuda-minor-version }}
platforms: ${{ matrix.platforms }}
platform-tag: ${{ matrix.platform-tag || '' }}
runs-on: ${{ matrix.runs-on }}
builder-base-image: ${{ matrix.builder-base-image || '' }}
base-image: ${{ matrix.base-image }}
backend: ${{ matrix.backend }}
dockerfile: ${{ matrix.dockerfile }}
skip-drivers: ${{ matrix.skip-drivers }}
context: ${{ matrix.context }}
ubuntu-version: ${{ matrix.ubuntu-version }}
amdgpu-targets: ${{ matrix.amdgpu-targets || 'gfx908,gfx90a,gfx942,gfx950,gfx1030,gfx1100,gfx1101,gfx1102,gfx1151,gfx1200,gfx1201' }}
secrets:
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: true
max-parallel: 8
matrix: ${{ fromJson(needs.generate-matrix.outputs['matrix-singlearch-2']) }}
backend-jobs-singlearch-3:
needs: generate-matrix
if: needs.generate-matrix.outputs['has-backends-singlearch-3'] == 'true'
uses: ./.github/workflows/backend_build.yml
with:
tag-latest: ${{ matrix.tag-latest }}
tag-suffix: ${{ matrix.tag-suffix }}
build-type: ${{ matrix.build-type }}
cuda-major-version: ${{ matrix.cuda-major-version }}
cuda-minor-version: ${{ matrix.cuda-minor-version }}
platforms: ${{ matrix.platforms }}
platform-tag: ${{ matrix.platform-tag || '' }}
runs-on: ${{ matrix.runs-on }}
builder-base-image: ${{ matrix.builder-base-image || '' }}
base-image: ${{ matrix.base-image }}
backend: ${{ matrix.backend }}
dockerfile: ${{ matrix.dockerfile }}
skip-drivers: ${{ matrix.skip-drivers }}
context: ${{ matrix.context }}
ubuntu-version: ${{ matrix.ubuntu-version }}
amdgpu-targets: ${{ matrix.amdgpu-targets || 'gfx908,gfx90a,gfx942,gfx950,gfx1030,gfx1100,gfx1101,gfx1102,gfx1151,gfx1200,gfx1201' }}
secrets:
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: true
max-parallel: 8
matrix: ${{ fromJson(needs.generate-matrix.outputs['matrix-singlearch-3']) }}
backend-jobs-singlearch-4:
needs: generate-matrix
if: needs.generate-matrix.outputs['has-backends-singlearch-4'] == 'true'
uses: ./.github/workflows/backend_build.yml
with:
tag-latest: ${{ matrix.tag-latest }}
tag-suffix: ${{ matrix.tag-suffix }}
build-type: ${{ matrix.build-type }}
cuda-major-version: ${{ matrix.cuda-major-version }}
cuda-minor-version: ${{ matrix.cuda-minor-version }}
platforms: ${{ matrix.platforms }}
platform-tag: ${{ matrix.platform-tag || '' }}
runs-on: ${{ matrix.runs-on }}
builder-base-image: ${{ matrix.builder-base-image || '' }}
base-image: ${{ matrix.base-image }}
backend: ${{ matrix.backend }}
dockerfile: ${{ matrix.dockerfile }}
skip-drivers: ${{ matrix.skip-drivers }}
context: ${{ matrix.context }}
ubuntu-version: ${{ matrix.ubuntu-version }}
amdgpu-targets: ${{ matrix.amdgpu-targets || 'gfx908,gfx90a,gfx942,gfx950,gfx1030,gfx1100,gfx1101,gfx1102,gfx1151,gfx1200,gfx1201' }}
secrets:
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: true
max-parallel: 8
matrix: ${{ fromJson(needs.generate-matrix.outputs['matrix-singlearch-4']) }}
backend-merge-jobs-multiarch:
needs: [generate-matrix, backend-jobs-multiarch]
# backend_merge.yml's push-side steps are all gated on
@@ -118,9 +219,9 @@ jobs:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs['merge-matrix-multiarch']) }}
backend-merge-jobs-singlearch:
needs: [generate-matrix, backend-jobs-singlearch]
if: ${{ !cancelled() && github.event_name != 'pull_request' && needs.generate-matrix.outputs['has-merges-singlearch'] == 'true' }}
backend-merge-jobs-singlearch-1:
needs: [generate-matrix, backend-jobs-singlearch-1]
if: ${{ !cancelled() && github.event_name != 'pull_request' && needs.generate-matrix.outputs['has-merges-singlearch-1'] == 'true' }}
uses: ./.github/workflows/backend_merge.yml
with:
tag-latest: ${{ matrix.tag-latest }}
@@ -130,7 +231,49 @@ jobs:
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs['merge-matrix-singlearch']) }}
matrix: ${{ fromJson(needs.generate-matrix.outputs['merge-matrix-singlearch-1']) }}
backend-merge-jobs-singlearch-2:
needs: [generate-matrix, backend-jobs-singlearch-2]
if: ${{ !cancelled() && github.event_name != 'pull_request' && needs.generate-matrix.outputs['has-merges-singlearch-2'] == 'true' }}
uses: ./.github/workflows/backend_merge.yml
with:
tag-latest: ${{ matrix.tag-latest }}
tag-suffix: ${{ matrix.tag-suffix }}
secrets:
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs['merge-matrix-singlearch-2']) }}
backend-merge-jobs-singlearch-3:
needs: [generate-matrix, backend-jobs-singlearch-3]
if: ${{ !cancelled() && github.event_name != 'pull_request' && needs.generate-matrix.outputs['has-merges-singlearch-3'] == 'true' }}
uses: ./.github/workflows/backend_merge.yml
with:
tag-latest: ${{ matrix.tag-latest }}
tag-suffix: ${{ matrix.tag-suffix }}
secrets:
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs['merge-matrix-singlearch-3']) }}
backend-merge-jobs-singlearch-4:
needs: [generate-matrix, backend-jobs-singlearch-4]
if: ${{ !cancelled() && github.event_name != 'pull_request' && needs.generate-matrix.outputs['has-merges-singlearch-4'] == 'true' }}
uses: ./.github/workflows/backend_merge.yml
with:
tag-latest: ${{ matrix.tag-latest }}
tag-suffix: ${{ matrix.tag-suffix }}
secrets:
quayUsername: ${{ secrets.LOCALAI_REGISTRY_USERNAME }}
quayPassword: ${{ secrets.LOCALAI_REGISTRY_PASSWORD }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs['merge-matrix-singlearch-4']) }}
backend-jobs-darwin:
needs: generate-matrix
uses: ./.github/workflows/backend_build_darwin.yml

View File

@@ -54,7 +54,7 @@ jobs:
go-version: ${{ matrix.go-version }}
cache: false
- name: Cache FizzBee
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: .tools/fizzbee
key: fizzbee-v0.5.2-${{ runner.os }}-${{ hashFiles('formal-verification/fizzbee.sha256') }}

View File

@@ -1,5 +1,5 @@
IK_LLAMA_VERSION?=bbc7de475178dd0535c16ad85f204a2529806c9d
IK_LLAMA_VERSION?=a8cf53fd69bada5450bd653eb0e32d1113fbd7fe
LLAMA_REPO?=https://github.com/ikawrakow/ik_llama.cpp
CMAKE_ARGS?=

View File

@@ -1,5 +1,5 @@
LLAMA_VERSION?=2da668617612d2df773f966e3b0ee22dc2beef7b
LLAMA_VERSION?=f36e5c348bc8795c34f9a038e58876e7a8423d4d
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
CMAKE_ARGS?=

View File

@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# CrispASR version (release tag)
CRISPASR_REPO?=https://github.com/CrispStrobe/CrispASR
CRISPASR_VERSION?=09df654e304947f7521e1f52992ceacccf03c300
CRISPASR_VERSION?=0a7643f1006c6bf2d1f37f5c63e9726f5eb4f364
SO_TARGET?=libgocrispasr.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF

View File

@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# qwentts.cpp version
QWEN3TTS_REPO?=https://github.com/ServeurpersoCom/qwentts.cpp
QWEN3TTS_CPP_VERSION?=73fe0c67bbf0898ba2999535e0680a02a7f8537d
QWEN3TTS_CPP_VERSION?=0725d2e53b7d2749a99ff33d3a460b954ffa7805
SO_TARGET?=libgoqwen3ttscpp.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF

View File

@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# stablediffusion.cpp (ggml)
STABLEDIFFUSION_GGML_REPO?=https://github.com/leejet/stable-diffusion.cpp
STABLEDIFFUSION_GGML_VERSION?=2574f5936571645f784b77623e1f09bad97d948a
STABLEDIFFUSION_GGML_VERSION?=bb84971129d2a094ab8051c6feed5406d3b4409d
CMAKE_ARGS+=-DGGML_MAX_NAME=128

View File

@@ -660,9 +660,16 @@ int load_model(const char *model, char *model_path, char* options[], int threads
ctx_params.diffusion_conv_direct = diffusion_conv_direct;
ctx_params.vae_conv_direct = vae_conv_direct;
ctx_params.force_sdxl_vae_conv_scale = force_sdxl_vae_conv_scale;
ctx_params.chroma_use_dit_mask = chroma_use_dit_mask;
ctx_params.chroma_use_t5_mask = chroma_use_t5_mask;
ctx_params.chroma_t5_mask_pad = chroma_t5_mask_pad;
// Chroma knobs: upstream dropped the dedicated chroma_use_dit_mask /
// chroma_use_t5_mask / chroma_t5_mask_pad struct fields and now reads them
// from the generic model_args key=value spec (parse_key_value_args). Emit
// them there so the existing chroma options keep working. This string must
// outlive new_sd_ctx() below.
std::string model_args_spec =
"chroma_use_dit_mask=" + std::string(chroma_use_dit_mask ? "true" : "false") +
",chroma_use_t5_mask=" + std::string(chroma_use_t5_mask ? "true" : "false") +
",chroma_t5_mask_pad=" + std::to_string(chroma_t5_mask_pad);
ctx_params.model_args = model_args_spec.c_str();
sd_ctx_t* sd_ctx = new_sd_ctx(&ctx_params);
if (sd_ctx == NULL) {

View File

@@ -119,7 +119,7 @@ if [ "$(uname -s)" = "Darwin" ]; then
# can rewrite it. Darwin therefore follows vllm-metal and can lag the Linux
# vllm pin (requirements-cublas13-after.txt, bumped independently against
# vllm/vllm) until vllm-metal supports a newer vLLM.
VLLM_METAL_VERSION="v0.3.0.dev20260704102955"
VLLM_METAL_VERSION="v0.3.0.dev20260706123649"
# The coupled vLLM source version is whatever this vllm-metal release builds
# against -- it declares it in its own installer as `vllm_v=`. Derive it from

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"slices"
"strings"
)
@@ -58,7 +59,7 @@ func (s *chatSession) Clear() {
}
func (s *chatSession) SwitchModel(model string) error {
if !modelExists(s.models, model) {
if !slices.Contains(s.models, model) {
return fmt.Errorf("model %q is not available. Use /models to see installed models", model)
}
s.model = model
@@ -103,18 +104,9 @@ Then start a chat session:
b.WriteString("multiple models are available; choose one with --model:\n")
b.WriteString(formatChatModelList(models, ""))
return "", errors.New(b.String())
case !modelExists(models, requested):
case !slices.Contains(models, requested):
return "", fmt.Errorf("model %q is not available. Use `local-ai models list` and `local-ai models install <model>`, or pass an installed model with --model", requested)
default:
return requested, nil
}
}
func modelExists(models []string, name string) bool {
for _, model := range models {
if model == name {
return true
}
}
return false
}

View File

@@ -23,6 +23,27 @@ import (
"github.com/mudler/LocalAI/pkg/httpclient"
)
// oidcSupportedSigningAlgs is the set of ID-token signature algorithms the OIDC
// verifier accepts. go-oidc defaults to RS256 only, which rejects identity
// providers that sign tokens with EC keys (ES*) or other algorithms — e.g.
// Authentik configured with an EC signing key fails the callback with
// "unexpected signature algorithm ... expected [RS256]" (#10677). All entries
// are asymmetric algorithms verified against the provider's published JWKS;
// HS256 is intentionally excluded (it is symmetric and would validate against
// the client secret, a different and security-sensitive trust model).
var oidcSupportedSigningAlgs = []string{
oidc.RS256, oidc.RS384, oidc.RS512,
oidc.ES256, oidc.ES384, oidc.ES512,
oidc.PS256, oidc.PS384, oidc.PS512,
oidc.EdDSA,
}
// OIDCSupportedSigningAlgs returns a copy of the ID-token signature algorithms
// the OIDC verifier accepts. Exposed for tests.
func OIDCSupportedSigningAlgs() []string {
return append([]string(nil), oidcSupportedSigningAlgs...)
}
// providerEntry holds the OAuth2/OIDC config for a single provider.
type providerEntry struct {
oauth2Config oauth2.Config
@@ -85,7 +106,10 @@ func NewOAuthManager(baseURL string, params OAuthParams) (*OAuthManager, error)
return nil, fmt.Errorf("OIDC discovery failed for %s: %w", params.OIDCIssuer, err)
}
verifier := provider.Verifier(&oidc.Config{ClientID: params.OIDCClientID})
verifier := provider.Verifier(&oidc.Config{
ClientID: params.OIDCClientID,
SupportedSigningAlgs: oidcSupportedSigningAlgs,
})
m.providers[ProviderOIDC] = &providerEntry{
name: ProviderOIDC,

View File

@@ -0,0 +1,101 @@
//go:build auth
package auth_test
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/mudler/LocalAI/core/http/auth"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
const (
testOIDCIssuer = "https://issuer.example.test"
testOIDCClientID = "localai-test-client"
)
// signES256IDToken builds a minimal ES256-signed JWT in JOSE compact form. The
// signature is the raw R||S encoding (32 bytes each for P-256) that OIDC
// verifiers expect, not ASN.1/DER.
func signES256IDToken(key *ecdsa.PrivateKey, claims map[string]any) string {
b64 := func(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) }
header := b64([]byte(`{"alg":"ES256","typ":"JWT"}`))
payloadJSON, err := json.Marshal(claims)
Expect(err).NotTo(HaveOccurred())
signingInput := header + "." + b64(payloadJSON)
digest := sha256.Sum256([]byte(signingInput))
r, s, err := ecdsa.Sign(rand.Reader, key, digest[:])
Expect(err).NotTo(HaveOccurred())
sig := make([]byte, 64)
r.FillBytes(sig[:32])
s.FillBytes(sig[32:])
return signingInput + "." + b64(sig)
}
var _ = Describe("OIDC ID-token signing algorithms", func() {
var (
ctx context.Context
key *ecdsa.PrivateKey
idToken string
keySet *oidc.StaticKeySet
)
BeforeEach(func() {
ctx = context.Background()
var err error
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
Expect(err).NotTo(HaveOccurred())
idToken = signES256IDToken(key, map[string]any{
"iss": testOIDCIssuer,
"sub": "user-1",
"aud": testOIDCClientID,
"email": "user@example.test",
"iat": time.Now().Unix(),
"exp": time.Now().Add(time.Hour).Unix(),
})
keySet = &oidc.StaticKeySet{PublicKeys: []crypto.PublicKey{key.Public()}}
})
It("accepts an EC (ES256) signed ID token with the configured algorithms", func() {
verifier := oidc.NewVerifier(testOIDCIssuer, keySet, &oidc.Config{
ClientID: testOIDCClientID,
SupportedSigningAlgs: auth.OIDCSupportedSigningAlgs(),
})
tok, err := verifier.Verify(ctx, idToken)
Expect(err).NotTo(HaveOccurred())
Expect(tok.Subject).To(Equal("user-1"))
})
It("rejects the same token under go-oidc's RS256-only default (the pre-fix behavior)", func() {
// This reproduces #10677: without a broadened SupportedSigningAlgs the
// verifier only accepts RS256 and rejects EC-signed tokens.
verifier := oidc.NewVerifier(testOIDCIssuer, keySet, &oidc.Config{
ClientID: testOIDCClientID,
})
_, err := verifier.Verify(ctx, idToken)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("ES256"))
})
It("advertises the common asymmetric algorithms and excludes the symmetric HS256", func() {
algs := auth.OIDCSupportedSigningAlgs()
Expect(algs).To(ContainElements(
oidc.RS256, oidc.RS384, oidc.RS512,
oidc.ES256, oidc.ES384, oidc.ES512,
oidc.PS256, oidc.EdDSA,
))
Expect(algs).NotTo(ContainElement("HS256"))
})
})

View File

@@ -1,3 +1,3 @@
{
"version": "v4.6.0"
"version": "v4.6.2"
}

View File

@@ -13475,6 +13475,223 @@
- filename: minicpm-v-4_5-mmproj-f16.gguf
sha256: 7a7225a32e8d453aaa3d22d8c579b5bf833c253f784cdb05c99c9a76fd616df8
uri: huggingface://openbmb/MiniCPM-V-4_5-gguf/mmproj-model-f16.gguf
- name: minicpm-v-4_6
url: github:mudler/LocalAI/gallery/qwen3.yaml@master
urls:
- https://huggingface.co/openbmb/MiniCPM-V-4.6-gguf
- https://huggingface.co/openbmb/MiniCPM-V-4.6
description: |
MiniCPM-V 4.6 is the most edge-deployment-friendly model in the MiniCPM-V series, with a total of 1.3B parameters. Built on Qwen3.5-0.8B and SigLIP2-400M, it features ultra-efficient architecture with mixed 4x/16x visual token compression for on-device deployment on iOS, Android, and HarmonyOS.
license: apache-2.0
icon: https://avatars.githubusercontent.com/u/89920203
tags:
- llm
- multimodal
- gguf
- gpu
- qwen3
- cpu
overrides:
mmproj: minicpm-v-4_6-mmproj-f16.gguf
parameters:
model: minicpm-v-4_6-Q4_K_M.gguf
files:
- filename: minicpm-v-4_6-Q4_K_M.gguf
sha256: 6b0c74962c44bc6bf4b655b9b02c13eda9d5a0491543ae976d1ac18e4b7892e2
uri: huggingface://openbmb/MiniCPM-V-4.6-gguf/MiniCPM-V-4_6-Q4_K_M.gguf
- filename: minicpm-v-4_6-mmproj-f16.gguf
sha256: ca931d861d0801d9003e50697cd764721a334107c0e0415a51168ee1938462de
uri: huggingface://openbmb/MiniCPM-V-4.6-gguf/mmproj-model-f16.gguf
- name: minicpm5-1b
url: github:mudler/LocalAI/gallery/chatml.yaml@master
urls:
- https://huggingface.co/openbmb/MiniCPM5-1B-GGUF
- https://huggingface.co/openbmb/MiniCPM5-1B
description: |
MiniCPM5-1B is a compact 1B-parameter language model based on the LLaMA architecture. Despite its small size, it achieves competitive performance among sub-2B models, making it ideal for edge deployment and resource-constrained environments.
license: apache-2.0
icon: https://avatars.githubusercontent.com/u/89920203
tags:
- llm
- gguf
- gpu
- cpu
overrides:
parameters:
model: MiniCPM5-1B-Q4_K_M.gguf
files:
- filename: MiniCPM5-1B-Q4_K_M.gguf
sha256: 81b64d05a23b17b34c475f42b3e72fbde62d4b92cc34541f7a8031d0752deafa
uri: huggingface://openbmb/MiniCPM5-1B-GGUF/MiniCPM5-1B-Q4_K_M.gguf
- name: minicpm-o-4_5
url: github:mudler/LocalAI/gallery/qwen3.yaml@master
urls:
- https://huggingface.co/openbmb/MiniCPM-o-4_5-gguf
- https://huggingface.co/openbmb/MiniCPM-o-4_5
description: |
MiniCPM-o 4.5 is the latest omni-modal model in the MiniCPM series. Built on Qwen3-8B and SigLIP2-400M, it supports vision, speech, and text in a unified 8B-parameter architecture with full-duplex real-time streaming.
license: apache-2.0
icon: https://avatars.githubusercontent.com/u/89920203
tags:
- llm
- multimodal
- gguf
- gpu
- qwen3
- cpu
overrides:
mmproj: minicpm-o-4_5-mmproj-f16.gguf
parameters:
model: minicpm-o-4_5-Q4_K_M.gguf
files:
- filename: minicpm-o-4_5-Q4_K_M.gguf
sha256: 1237a97ee081b8abebc47aa7dad565701e8f5f904cdc92f6723ac4281bbc0932
uri: huggingface://openbmb/MiniCPM-o-4_5-gguf/MiniCPM-o-4_5-Q4_K_M.gguf
- filename: minicpm-o-4_5-mmproj-f16.gguf
sha256: 1453678cc4e4fe18de241952962e234f265cb8dda780773526103ab8ba82f421
uri: huggingface://openbmb/MiniCPM-o-4_5-gguf/vision/MiniCPM-o-4_5-vision-F16.gguf
- name: minicpm-v-4_6-thinking
url: github:mudler/LocalAI/gallery/qwen3.yaml@master
urls:
- https://huggingface.co/openbmb/MiniCPM-V-4.6-Thinking-gguf
- https://huggingface.co/openbmb/MiniCPM-V-4.6-Thinking
description: |
MiniCPM-V 4.6 Thinking is the reasoning-enhanced version of MiniCPM-V 4.6, with a total of 1.3B parameters. Built on Qwen3.5-0.8B and SigLIP2-400M, it features extended thinking capabilities for complex visual reasoning tasks while maintaining an ultra-compact architecture for edge deployment.
license: apache-2.0
icon: https://avatars.githubusercontent.com/u/89920203
tags:
- llm
- multimodal
- gguf
- gpu
- qwen3
- cpu
overrides:
mmproj: minicpm-v-4_6-thinking-mmproj-f16.gguf
parameters:
model: MiniCPM-V-4_6-Thinking-Q4_K_M.gguf
files:
- filename: MiniCPM-V-4_6-Thinking-Q4_K_M.gguf
sha256: 2d15cea059289533a4e51cff895888a2afaf8ed0c88bcc11ba590d2a45c6f174
uri: huggingface://openbmb/MiniCPM-V-4.6-Thinking-gguf/MiniCPM-V-4_6-Thinking-Q4_K_M.gguf
- filename: minicpm-v-4_6-thinking-mmproj-f16.gguf
sha256: b9d09d261de167b291a69958b520c6411877cbff50e96a83d06e9835627c70f6
uri: huggingface://openbmb/MiniCPM-V-4.6-Thinking-gguf/mmproj-model-f16.gguf
- name: minicpm-v-4
url: github:mudler/LocalAI/gallery/chatml.yaml@master
urls:
- https://huggingface.co/openbmb/MiniCPM-V-4-gguf
- https://huggingface.co/openbmb/MiniCPM-V-4
description: |
MiniCPM-V 4 is a multimodal large language model in the MiniCPM-V series, supporting image and video understanding with strong OCR and multi-image capabilities.
license: apache-2.0
icon: https://avatars.githubusercontent.com/u/89920203
tags:
- llm
- multimodal
- gguf
- gpu
- cpu
overrides:
mmproj: minicpm-v-4-mmproj-f16.gguf
parameters:
model: minicpm-v-4-Q4_K_M.gguf
files:
- filename: minicpm-v-4-Q4_K_M.gguf
sha256: b0ff610e9c92b30389ff1e0dd40fffed3c1f02a9d34a735fd5fba6a5ad25672b
uri: huggingface://openbmb/MiniCPM-V-4-gguf/ggml-model-Q4_K_M.gguf
- filename: minicpm-v-4-mmproj-f16.gguf
sha256: f0faa9ae63532300999c86a196f140c716cd0fbb08bbbd81850f1f9a631f7761
uri: huggingface://openbmb/MiniCPM-V-4-gguf/mmproj-model-f16.gguf
- name: minicpm3-4b
url: github:mudler/LocalAI/gallery/chatml.yaml@master
urls:
- https://huggingface.co/openbmb/MiniCPM3-4B-GGUF
- https://huggingface.co/openbmb/MiniCPM3-4B
description: |
MiniCPM3-4B is a 4B-parameter language model that surpasses many larger models. It features enhanced long-context capability up to 32K tokens, strong function calling, and improved instruction following.
license: apache-2.0
icon: https://avatars.githubusercontent.com/u/89920203
tags:
- llm
- gguf
- gpu
- cpu
overrides:
parameters:
model: minicpm3-4b-q4_k_m.gguf
files:
- filename: minicpm3-4b-q4_k_m.gguf
sha256: 64913247e927414ecf47fd3e9ea8e3f0c9acae293f583dfa7e24b8872e20fa4c
uri: huggingface://openbmb/MiniCPM3-4B-GGUF/minicpm3-4b-q4_k_m.gguf
- name: minicpm-o-2_6
url: github:mudler/LocalAI/gallery/chatml.yaml@master
urls:
- https://huggingface.co/openbmb/MiniCPM-o-2_6-gguf
- https://huggingface.co/openbmb/MiniCPM-o-2_6
description: |
MiniCPM-o 2.6 is an omni-modal model supporting vision, speech, and text. Based on a 7.6B-parameter architecture with SigLIP-400M and Whisper-medium, it offers real-time speech conversation and multimodal live streaming capabilities.
license: apache-2.0
icon: https://avatars.githubusercontent.com/u/89920203
tags:
- llm
- multimodal
- gguf
- gpu
- cpu
overrides:
mmproj: minicpm-o-2_6-mmproj-f16.gguf
parameters:
model: minicpm-o-2_6-Q4_K_M.gguf
files:
- filename: minicpm-o-2_6-Q4_K_M.gguf
sha256: 4f635fc0c0bb88d50ccd9cf1f1e5892b5cb085ff88fe0d8e1148fd9a8a836bc2
uri: huggingface://openbmb/MiniCPM-o-2_6-gguf/Model-7.6B-Q4_K_M.gguf
- filename: minicpm-o-2_6-mmproj-f16.gguf
sha256: efa4f7d96aa0f838f2023fc8d28e519179b16f1106777fa9280b32628191aa3e
uri: huggingface://openbmb/MiniCPM-o-2_6-gguf/mmproj-model-f16.gguf
- name: minicpm4_1-8b
url: github:mudler/LocalAI/gallery/chatml.yaml@master
urls:
- https://huggingface.co/openbmb/MiniCPM4.1-8B-GGUF
- https://huggingface.co/openbmb/MiniCPM4.1-8B
description: |
MiniCPM4.1-8B is an 8B-parameter language model with enhanced capabilities in coding, mathematics, and instruction following, building upon the MiniCPM4 architecture.
license: apache-2.0
icon: https://avatars.githubusercontent.com/u/89920203
tags:
- llm
- gguf
- gpu
- cpu
overrides:
parameters:
model: MiniCPM4.1-8B-Q4_K_M.gguf
files:
- filename: MiniCPM4.1-8B-Q4_K_M.gguf
sha256: 9d2ffb9145bf7a88ddb94b2542b42d925293666c42962aa158eeb62fc9708654
uri: huggingface://openbmb/MiniCPM4.1-8B-GGUF/MiniCPM4.1-8B-Q4_K_M.gguf
- name: minicpm4-8b
url: github:mudler/LocalAI/gallery/chatml.yaml@master
urls:
- https://huggingface.co/openbmb/MiniCPM4-8B-GGUF
- https://huggingface.co/openbmb/MiniCPM4-8B
description: |
MiniCPM4-8B is an 8B-parameter language model achieving competitive performance among models of similar size, with strong capabilities in reasoning, coding, and multilingual tasks.
license: apache-2.0
icon: https://avatars.githubusercontent.com/u/89920203
tags:
- llm
- gguf
- gpu
- cpu
overrides:
parameters:
model: MiniCPM4-8B-Q4_K_M.gguf
files:
- filename: MiniCPM4-8B-Q4_K_M.gguf
sha256: 80b99ec4ceac302e7d474c8659e96b9f6c3bbeb73c41b2ef841c7fcb6d8250f3
uri: huggingface://openbmb/MiniCPM4-8B-GGUF/MiniCPM4-8B-Q4_K_M.gguf
- name: aquif-ai_aquif-3.5-8b-think
url: github:mudler/LocalAI/gallery/qwen3.yaml@master
urls:

View File

@@ -2,12 +2,18 @@ package vram
import (
"context"
"regexp"
"strings"
"sync"
hfapi "github.com/mudler/LocalAI/pkg/huggingface-api"
)
// ggufShardRe matches the multi-part suffix used for sharded GGUF files, e.g.
// "model-00001-of-00003.gguf". Stripping it collapses all shards of one
// quantization onto a single variant key.
var ggufShardRe = regexp.MustCompile(`-\d{2,5}-of-\d{2,5}$`)
var (
hfSizeCacheMu sync.Mutex
hfSizeCacheData = make(map[string]hfSizeCacheEntry)
@@ -107,18 +113,33 @@ func hfRepoWeightSizeUncached(ctx context.Context, repoID string) (uint64, error
}
}
// sumWeightFileBytes estimates the on-disk size of the weights a user would
// actually download for a repo.
//
// A single HF repo very often ships many GGUF quantizations of the same model
// (Q4_K_M, Q5_K_M, Q8_0, F16, ...). These are mutually-exclusive alternatives:
// only one is ever downloaded and run, so summing the whole repo drastically
// over-reports the size (e.g. a 9B model shown as 71 GB). For GGUF we therefore
// group files by quantization variant -- summing the shards that belong to one
// variant -- and report the largest single variant as a conservative estimate.
//
// Non-GGUF weights (.safetensors/.bin/.pt) are genuine shards of one model that
// are all required together, so those are still summed.
func sumWeightFileBytes(files []hfapi.FileInfo) uint64 {
var total int64
var nonGGUFTotal int64
ggufVariants := make(map[string]int64)
haveGGUF := false
for _, f := range files {
if f.Type != "file" {
continue
}
ext := strings.ToLower(f.Path)
if idx := strings.LastIndex(ext, "."); idx >= 0 {
ext = ext[idx:]
} else {
lower := strings.ToLower(f.Path)
idx := strings.LastIndex(lower, ".")
if idx < 0 {
continue
}
ext := lower[idx:]
if !weightExts[ext] {
continue
}
@@ -126,10 +147,32 @@ func sumWeightFileBytes(files []hfapi.FileInfo) uint64 {
if f.LFS != nil && f.LFS.Size > 0 {
size = f.LFS.Size
}
total += size
if size < 0 {
continue
}
if ext == ".gguf" {
haveGGUF = true
variant := ggufShardRe.ReplaceAllString(lower[:idx], "")
ggufVariants[variant] += size
} else {
nonGGUFTotal += size
}
}
if total < 0 {
if haveGGUF {
// When GGUF is present it is what LocalAI runs; report the single
// largest quantization variant instead of the whole repository.
var largest int64
for _, v := range ggufVariants {
if v > largest {
largest = v
}
}
return uint64(largest)
}
if nonGGUFTotal < 0 {
return 0
}
return uint64(total)
return uint64(nonGGUFTotal)
}

View File

@@ -0,0 +1,52 @@
package vram
import (
hfapi "github.com/mudler/LocalAI/pkg/huggingface-api"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func file(path string, size int64) hfapi.FileInfo {
return hfapi.FileInfo{Type: "file", Path: path, Size: size}
}
var _ = Describe("sumWeightFileBytes", func() {
It("reports only the largest quantization for a multi-GGUF repo", func() {
// A single repo shipping several mutually-exclusive quantizations.
files := []hfapi.FileInfo{
file("model-Q4_K_M.gguf", 5_000),
file("model-Q5_K_M.gguf", 6_000),
file("model-Q8_0.gguf", 9_000),
file("README.md", 100),
file(".gitattributes", 10),
}
Expect(sumWeightFileBytes(files)).To(Equal(uint64(9_000)))
})
It("sums shards that belong to the same GGUF variant", func() {
files := []hfapi.FileInfo{
file("model-Q8_0-00001-of-00003.gguf", 4_000),
file("model-Q8_0-00002-of-00003.gguf", 4_000),
file("model-Q8_0-00003-of-00003.gguf", 4_000),
file("model-Q4_K_M.gguf", 5_000),
}
// Q8_0 variant totals 12_000, larger than the single Q4_K_M file.
Expect(sumWeightFileBytes(files)).To(Equal(uint64(12_000)))
})
It("still sums all shards for a safetensors-only repo", func() {
files := []hfapi.FileInfo{
file("model-00001-of-00003.safetensors", 3_000),
file("model-00002-of-00003.safetensors", 3_000),
file("model-00003-of-00003.safetensors", 3_000),
}
Expect(sumWeightFileBytes(files)).To(Equal(uint64(9_000)))
})
It("prefers LFS size when present", func() {
files := []hfapi.FileInfo{
{Type: "file", Path: "model-Q4_K_M.gguf", Size: 133, LFS: &hfapi.LFSInfo{Size: 7_000}},
}
Expect(sumWeightFileBytes(files)).To(Equal(uint64(7_000)))
})
})

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"os/exec"
"slices"
"strings"
"sync"
"time"
@@ -49,12 +50,7 @@ type clinfoTypeProp struct {
}
func (t clinfoTypeProp) isGPU() bool {
for _, s := range t.Type {
if s == clDeviceTypeGPU {
return true
}
}
return false
return slices.Contains(t.Type, clDeviceTypeGPU)
}
// clinfoOnce caches the result for the process lifetime. GPU hardware

View File

@@ -209,23 +209,75 @@ function splitByArch(entries) {
return { multiarch, singlearch };
}
// GitHub Actions refuses to instantiate a matrix with more than 256 jobs. When
// it happens the job doesn't error visibly — it hangs forever at "Waiting for
// pending jobs" and the whole run is marked `failure` while every *other* job
// stays green (seen on the v4.6.1 tag build, run 28786533892: 268 single-arch
// entries, zero single-arch jobs ever created). The single-arch list is the
// one that grows unbounded as backends are added, so we shard it across a
// fixed number of matrix jobs instead of feeding one oversized matrix.
//
// SINGLEARCH_SHARDS MUST equal the number of backend-jobs-singlearch-<n>
// (and backend-merge-jobs-singlearch-<n>) blocks defined in backend.yml and
// backend_pr.yml. Bump all three together.
const SINGLEARCH_SHARDS = 4;
const GHA_MATRIX_LIMIT = 256;
// Split `arr` into exactly `shards` balanced, contiguous chunks. Earlier chunks
// absorb the remainder when the length doesn't divide evenly; trailing chunks
// may be empty when there are fewer entries than shards (those emit a
// has-backends-singlearch-<n>=false flag so their job is skipped).
function chunkEqually(arr, shards) {
const out = [];
const base = Math.floor(arr.length / shards);
const rem = arr.length % shards;
let idx = 0;
for (let i = 0; i < shards; i++) {
const size = base + (i < rem ? 1 : 0);
out.push(arr.slice(idx, idx + size));
idx += size;
}
return out;
}
// Emit the sharded single-arch build + merge matrices and their has-* gates.
// Called with the full or filtered single-arch entry list.
function emitSinglearchShards(singlearch) {
const shards = chunkEqually(singlearch, SINGLEARCH_SHARDS);
for (let i = 0; i < SINGLEARCH_SHARDS; i++) {
const shard = shards[i];
// Fail loudly rather than let GitHub silently drop the overflow: a shard at
// or above the limit means SINGLEARCH_SHARDS (and the matching job blocks in
// both workflows) need to grow.
if (shard.length >= GHA_MATRIX_LIMIT) {
throw new Error(
`single-arch shard ${i + 1} has ${shard.length} entries (>= ${GHA_MATRIX_LIMIT}, ` +
`GitHub's per-matrix job limit). Increase SINGLEARCH_SHARDS in ` +
`scripts/changed-backends.js and add matching backend-jobs-singlearch-<n> / ` +
`backend-merge-jobs-singlearch-<n> blocks to backend.yml and backend_pr.yml.`
);
}
const merge = computeMergeMatrix(shard);
const n = i + 1;
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-singlearch-${n}=${shard.length > 0 ? 'true' : 'false'}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-merges-singlearch-${n}=${merge.include.length > 0 ? 'true' : 'false'}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-singlearch-${n}=${JSON.stringify({ include: shard })}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `merge-matrix-singlearch-${n}=${JSON.stringify(merge)}\n`);
}
}
function emitFullMatrix() {
const { multiarch, singlearch } = splitByArch(includes);
const mergeMatrixMultiarch = computeMergeMatrix(multiarch);
const mergeMatrixSinglearch = computeMergeMatrix(singlearch);
const hasMergesMultiarch = mergeMatrixMultiarch.include.length > 0 ? 'true' : 'false';
const hasMergesSinglearch = mergeMatrixSinglearch.include.length > 0 ? 'true' : 'false';
fs.appendFileSync(process.env.GITHUB_OUTPUT, `run-all=true\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-singlearch=${singlearch.length > 0 ? 'true' : 'false'}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-multiarch=${multiarch.length > 0 ? 'true' : 'false'}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-darwin=true\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-merges-multiarch=${hasMergesMultiarch}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-merges-singlearch=${hasMergesSinglearch}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-singlearch=${JSON.stringify({ include: singlearch })}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-multiarch=${JSON.stringify({ include: multiarch })}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-darwin=${JSON.stringify({ include: includesDarwin })}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `merge-matrix-multiarch=${JSON.stringify(mergeMatrixMultiarch)}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `merge-matrix-singlearch=${JSON.stringify(mergeMatrixSinglearch)}\n`);
emitSinglearchShards(singlearch);
for (const backend of allBackendPaths.keys()) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${backend}=true\n`);
}
@@ -249,29 +301,23 @@ function emitFilteredMatrix(changedFiles) {
console.log("Filtered files Darwin:", filteredDarwin);
const { multiarch, singlearch } = splitByArch(filtered);
const hasBackendsSinglearch = singlearch.length > 0 ? 'true' : 'false';
const hasBackendsMultiarch = multiarch.length > 0 ? 'true' : 'false';
const hasBackendsDarwin = filteredDarwin.length > 0 ? 'true' : 'false';
console.log("Has single-arch backends?:", hasBackendsSinglearch);
console.log("Has single-arch backends?:", singlearch.length > 0 ? 'true' : 'false');
console.log("Has multi-arch backends?:", hasBackendsMultiarch);
console.log("Has Darwin backends?:", hasBackendsDarwin);
const mergeMatrixMultiarch = computeMergeMatrix(multiarch);
const mergeMatrixSinglearch = computeMergeMatrix(singlearch);
const hasMergesMultiarch = mergeMatrixMultiarch.include.length > 0 ? 'true' : 'false';
const hasMergesSinglearch = mergeMatrixSinglearch.include.length > 0 ? 'true' : 'false';
fs.appendFileSync(process.env.GITHUB_OUTPUT, `run-all=false\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-singlearch=${hasBackendsSinglearch}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-multiarch=${hasBackendsMultiarch}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-backends-darwin=${hasBackendsDarwin}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-merges-multiarch=${hasMergesMultiarch}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has-merges-singlearch=${hasMergesSinglearch}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-singlearch=${JSON.stringify({ include: singlearch })}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-multiarch=${JSON.stringify({ include: multiarch })}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix-darwin=${JSON.stringify({ include: filteredDarwin })}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `merge-matrix-multiarch=${JSON.stringify(mergeMatrixMultiarch)}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `merge-matrix-singlearch=${JSON.stringify(mergeMatrixSinglearch)}\n`);
emitSinglearchShards(singlearch);
// Per-backend boolean outputs
for (const [backend, pathPrefix] of allBackendPaths) {