Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/build-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,35 @@ jobs:
- name: Run GoReleaser
run: |
make ${{ github.event_name == 'pull_request' && 'dev-dist-single' || 'dev-dist' }}
# Windows ships no GNU toolchain on the default runner: make is not
# installed there and Git for Windows does not ship it (it bundles sh, uname,
# unzip, grep, awk, sed only), so it is installed via Chocolatey below. The
# Makefile finds Git for Windows' sh itself and runs recipes through it (see
# the SHELL setup at the top of the Makefile), so the explicit usr/bin PATH
# step below is belt-and-suspenders for environments where make must locate
# sh before the makefile is read. The server builds with CGO_ENABLED=0, so no
# mingw/gcc is needed; the Makefile downloads protoc and installs the Go
# protobuf plugins itself (see the protoc/protogen-go targets).
build-test-windows:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.25
- name: Add Git usr/bin to PATH
run: |
echo "$env:ProgramFiles\Git\usr\bin" | Out-File -Append -Encoding utf8 $env:GITHUB_PATH
- name: Install GNU Make
run: choco install make -y
- name: Build LocalAI (CGO disabled, mirrors release config)
env:
CGO_ENABLED: '0'
run: make build
launcher-build-darwin:
runs-on: macos-latest
steps:
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ prepare-sources
/backend-images
/result.yaml
protoc
protoc.exe

*.log

Expand All @@ -25,6 +26,7 @@ go-bert
# LocalAI build binary
LocalAI
/local-ai
/local-ai.exe
/local-ai-launcher
# Root-level build artifacts when running `go build ./...` against
# Go backend packages whose main lives under backend/go/.
Expand Down Expand Up @@ -129,3 +131,6 @@ formal-verification/out/
# root, which is what a contributor testing a build does. Nothing under here is
# source: it is the instance's own models, outputs, traces and identity.
/data/

# Model gallery index cache fetched by `local-ai` at runtime (cache/gallery/).
/cache/
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ builds:
goos:
- linux
- darwin
#- windows
- windows
goarch:
- amd64
- arm64
Expand Down
46 changes: 37 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
# Disable parallel execution for backend builds
.NOTPARALLEL: backends/diffusers backends/llama-cpp backends/turboquant backends/bonsai backends/outetts backends/piper backends/stablediffusion-ggml backends/trellis2cpp backends/trellis2cpp-darwin backends/whisper backends/crispasr backends/parakeet-cpp backends/moss-transcribe-cpp backends/nemo-speech-cpp backends/faster-whisper backends/silero-vad backends/local-store backends/valkey-store backends/cloud-proxy backends/huggingface backends/rfdetr backends/rfdetr-cpp backends/insightface backends/speaker-recognition backends/kitten-tts backends/kokoro backends/chatterbox backends/llama-cpp-darwin backends/neutts build-darwin-python-backend build-darwin-go-backend backends/mlx backends/diffuser-darwin backends/mlx-vlm backends/mlx-audio backends/mlx-distributed backends/stablediffusion-ggml-darwin backends/vllm backends/vllm-omni backends/longcat-video backends/sglang backends/moonshine backends/pocket-tts backends/qwen-tts backends/faster-qwen3-tts backends/qwen-asr backends/nemo backends/voxcpm backends/whisperx backends/ace-step backends/acestep-cpp backends/fish-speech backends/voxtral backends/opus backends/trl backends/llama-cpp-quantization backends/kokoros backends/sam3-cpp backends/qwen3-tts-cpp backends/moss-tts-cpp backends/magpie-tts-cpp backends/vllm-cpp backends/omnivoice-cpp backends/vibevoice-cpp backends/localvqe backends/tinygrad backends/sherpa-onnx backends/ds4 backends/ds4-darwin backends/liquid-audio backends/supertonic backends/depth-anything-cpp backends/privacy-filter backends/privacy-filter-darwin backends/audio-cpp backends/audio-cpp-darwin

# Native GNU make for Windows (e.g. ezwinports) only behaves like the Unix
# make when it can find sh.exe: otherwise recipe lines run under cmd.exe and
# $(shell ...) degrades to bare CreateProcess, which breaks every POSIX
# recipe and the uname/tput expansion below. Seed the exported PATH with the
# sh directories from the standard Git-for-Windows / MSYS2 installs (missing
# entries are harmless in a Windows PATH) and force SHELL to sh, mirroring
# what CI does. Detection uses the cmd environment OS variable (Windows_NT);
# OS is re-derived from uname further down. Nothing matches on
# Linux/macOS/WSL, so the block is inert there.
ifeq ($(findstring Windows,$(OS)),Windows)
export PATH := C:/Program Files/Git/usr/bin;$(if $(LOCALAPPDATA),$(LOCALAPPDATA)/Programs/Git/usr/bin,);C:/msys64/usr/bin;$(PATH)
export SHELL := sh
endif

GOCMD=go
GOTEST=$(GOCMD) test
GOVET=$(GOCMD) vet
BINARY_NAME=local-ai
# Windows builds get the .exe suffix so the artifact is runnable from cmd /
# PowerShell (an extensionless PE needs a POSIX shell to launch it). OS is
# assigned below via uname; this is a recursive = so the suffix is expanded
# at use time, after OS exists. Empty on Linux/macOS, so nothing else changes.
BINARY_NAME=local-ai$(if $(findstring NT,$(OS)),.exe)
LAUNCHER_BINARY_NAME=local-ai-launcher

UBUNTU_VERSION?=2404
Expand Down Expand Up @@ -532,10 +550,18 @@ help: ## Show this help.
.PHONY: protogen
protogen: protogen-go

# The win64 protoc zip ships bin/protoc.exe while the unix zips ship
# bin/protoc. protogen-go invokes a bare ./protoc, and the protoc target must
# stay up-to-date once the binary is in place, so on Windows we extract the
# .exe and rename it to ./protoc (MSYS sh runs extensionless PE binaries).
PROTOC_MEMBER := bin/protoc$(if $(findstring NT,$(OS)),.exe)

protoc:
@OS_NAME=$$(uname -s | tr '[:upper:]' '[:lower:]'); \
ARCH_NAME=$$(uname -m); \
if [ "$$OS_NAME" = "darwin" ]; then \
if echo "$$OS_NAME" | grep -qE 'mingw|msys|cygwin'; then \
FILE=protoc-31.1-win64.zip; \
elif [ "$$OS_NAME" = "darwin" ]; then \
if [ "$$ARCH_NAME" = "arm64" ]; then \
FILE=protoc-31.1-osx-aarch_64.zip; \
elif [ "$$ARCH_NAME" = "x86_64" ]; then \
Expand All @@ -562,18 +588,20 @@ protoc:
fi; \
URL=https://github.com/protocolbuffers/protobuf/releases/download/v31.1/$$FILE; \
curl -L $$URL -o protoc.zip && \
unzip -j -d $(CURDIR) protoc.zip bin/protoc && rm protoc.zip
unzip -o -j -d $(CURDIR) protoc.zip $(PROTOC_MEMBER) && \
rm -f protoc.zip && \
[ ! -f ./protoc.exe ] || mv -f ./protoc.exe ./protoc

.PHONY: protogen-go
protogen-go: protoc install-go-tools
mkdir -p pkg/grpc/proto
# install-go-tools writes protoc-gen-go and protoc-gen-go-grpc into
# $(shell go env GOPATH)/bin, which isn't on every dev's PATH. protoc
# resolves its code-gen plugins via PATH, so without this prefix the
# generate step fails with "protoc-gen-go: program not found". Prepend
# GOPATH/bin so the freshly-installed plugins win without requiring a
# shell-profile change.
PATH="$$(go env GOPATH)/bin:$$PATH" ./protoc --experimental_allow_proto3_optional -Ibackend/ --go_out=pkg/grpc/proto/ --go_opt=paths=source_relative --go-grpc_out=pkg/grpc/proto/ --go-grpc_opt=paths=source_relative \
# $(shell go env GOPATH)/bin, which isn't on every dev's PATH. Point
# protoc at the plugins explicitly (--plugin) so discovery doesn't depend
# on PATH separator conventions (POSIX ':' vs Windows ';').
./protoc --experimental_allow_proto3_optional -Ibackend/ --go_out=pkg/grpc/proto/ --go_opt=paths=source_relative --go-grpc_out=pkg/grpc/proto/ --go-grpc_opt=paths=source_relative \
--plugin=protoc-gen-go="$$(go env GOPATH)/bin/protoc-gen-go$(if $(findstring NT,$(OS)),.exe,)" \
--plugin=protoc-gen-go-grpc="$$(go env GOPATH)/bin/protoc-gen-go-grpc$(if $(findstring NT,$(OS)),.exe,)" \
backend/backend.proto

core/config/inference_defaults.json: ## Fetch inference defaults from unsloth (only if missing)
Expand Down
2 changes: 1 addition & 1 deletion core/gallery/gallery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ var _ = Describe("Gallery", func() {
},
},
}
result := FindGalleryElement(modelsWithPath, "bert/embeddings")
result := FindGalleryElement(modelsWithPath, "bert"+string(os.PathSeparator)+"embeddings")
Expect(result).NotTo(BeNil())
Expect(result.GetName()).To(Equal("bert__embeddings"))
})
Expand Down
2 changes: 1 addition & 1 deletion core/gallery/model_artifacts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ unknown_extension:
Expect(err).NotTo(HaveOccurred())
Expect(fake.seen).To(HaveLen(1))
Expect(installed.Model).To(Equal("owner/repo"))
Expect(installed.ModelFileName()).To(Equal(fake.result.RelativePath))
Expect(installed.ModelFileName()).To(Equal(filepath.FromSlash(fake.result.RelativePath)))

data, err := os.ReadFile(filepath.Join(modelsPath, "managed.yaml"))
Expect(err).NotTo(HaveOccurred())
Expand Down
7 changes: 6 additions & 1 deletion core/http/endpoints/localai/video_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/base64"
"os"
"path/filepath"
"runtime"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand All @@ -23,7 +24,11 @@ var _ = Describe("video media staging", func() {
Expect(os.ReadFile(path)).To(Equal(content))
info, err := os.Stat(path)
Expect(err).NotTo(HaveOccurred())
Expect(info.Mode().Perm()).To(Equal(os.FileMode(0o600)))
// POSIX permission bits are not enforceable on Windows, where the file
// is reported with the default 0666 mode regardless of chmod.
if runtime.GOOS != "windows" {
Expect(info.Mode().Perm()).To(Equal(os.FileMode(0o600)))
}
})

It("accepts browser data URIs with codec parameters", func() {
Expand Down
3 changes: 3 additions & 0 deletions core/services/testutil/testdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ func SetupTestDB() *gorm.DB {
if runtime.GOOS == "darwin" {
Skip("testcontainers requires Docker, not available on macOS CI")
}
if runtime.GOOS == "windows" {
Skip("testcontainers requires Docker, not available on Windows CI")
}
ctx := context.Background()
pgC, err := tcpostgres.Run(ctx, "postgres:16",
tcpostgres.WithDatabase("testdb"),
Expand Down
34 changes: 34 additions & 0 deletions docs/content/getting-started/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@1958fcbe2ca8bd93af633f1

```

{{% /tab %}}
{{% tab title="Windows" %}}

The `make` recipes are POSIX shell. Git for Windows bundles the POSIX tools they need (`sh`, `uname`, `unzip`, `grep`, `awk`, `sed`) — but **not** `make` itself — so install a GNU make for Windows as well:

```powershell
winget install ezwinports.make
```

The Makefile locates Git for Windows' `sh` on its own and runs every recipe through it, downloads `protoc` and installs the Go protobuf plugins itself — so no PATH edits or extra toolchain are needed. `make build` also compiles the React UI, so install [Node.js](https://nodejs.org) (with npm) as well.

{{% /tab %}}
{{% tab title="From source" %}}

Expand All @@ -72,6 +83,8 @@ make build

This should produce the binary `local-ai`

On Windows, `make build` produces `local-ai.exe` instead.

#### Container image

Requirements:
Expand Down Expand Up @@ -143,6 +156,27 @@ make clean
make build
```

### Example: Build on Windows

Building the server binary on Windows needs no C toolchain: the release config compiles with `CGO_ENABLED=0`. The `make` recipes are POSIX shell, so you need Git for Windows (which bundles `sh` and the POSIX tools) plus a GNU make for Windows — Git for Windows does not ship `make`. The Makefile finds `sh` on its own, so no PATH edits are required.

```powershell
# Install Go (https://go.dev/dl), Git for Windows (https://git-scm.com/downloads),
# Node.js for the React UI build (https://nodejs.org), then a GNU make for
# Windows, e.g.:
winget install ezwinports.make

# Build with CGO disabled like the release config.
$env:CGO_ENABLED = '0'

git clone https://github.com/mudler/LocalAI.git
cd LocalAI

make build
```

This produces the binary `local-ai.exe`. Backends such as `llama-cpp` still build as Linux container images and are not distributed for Windows.

## Build backends

LocalAI have several backends available for installation in the backend gallery. The backends can be also built by source. As backends might vary from language and dependencies that they require, the documentation will provide generic guidance for few of the backends, which can be applied with some slight modifications also to the others.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/getting-started/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ curl http://localhost:8080/v1/completions -H "Content-Type: application/json" -d
For other Docker images, please refer to the table in [Getting Started](https://localai.io/basics/getting_started/#container-images).
{{% /notice %}}

Note: If you are on Windows, ensure the project is on the Linux filesystem to avoid slow model loading. For more information, see the [Microsoft Docs](https://learn.microsoft.com/en-us/windows/wsl/filesystems).
Note: When running LocalAI under Docker/WSL2 on Windows, ensure the project is on the Linux filesystem to avoid slow model loading. For more information, see the [Microsoft Docs](https://learn.microsoft.com/en-us/windows/wsl/filesystems).

{{% /tab %}}
{{% tab title="Kubernetes" %}}
Expand Down
2 changes: 1 addition & 1 deletion docs/content/getting-started/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi

```bash
chmod +x local-ai-*
./local-ai-Linux-x86_64 run
./local-ai-<version>-linux-amd64 run
```

If you see "cannot execute binary file: Exec format error", you downloaded the wrong architecture. Verify with:
Expand Down
16 changes: 10 additions & 6 deletions docs/content/reference/binaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title = "LocalAI binaries"
weight = 26
+++

LocalAI binaries are available for both Linux and MacOS platforms and can be executed directly from your command line. These binaries are continuously updated and hosted on [our GitHub Releases page](https://github.com/mudler/LocalAI/releases). This method also supports Windows users via the Windows Subsystem for Linux (WSL).
LocalAI binaries are available for Linux, macOS, and Windows platforms and can be executed directly from your command line. These binaries are continuously updated and hosted on [our GitHub Releases page](https://github.com/mudler/LocalAI/releases).

### macOS Download

Expand All @@ -17,19 +17,22 @@ You can download the DMG and install the application:

> Note: the DMGs are not signed by Apple as quarantined. See https://github.com/mudler/LocalAI/issues/6268 for a workaround, fix is tracked here: https://github.com/mudler/LocalAI/issues/6244

Otherwise, use the following one-liner command in your terminal to download and run LocalAI on Linux or MacOS:
Otherwise, use the following one-liner command in your terminal to download and run LocalAI on Linux or MacOS (set `VERSION` to the current tag, e.g. `v4.8.2`):

```bash
curl -Lo local-ai "https://github.com/mudler/LocalAI/releases/download/{{< version >}}/local-ai-$(uname -s)-$(uname -m)" && chmod +x local-ai && ./local-ai
VERSION=v4.8.2; ARCH=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/'); \
curl -Lo local-ai "https://github.com/mudler/LocalAI/releases/download/$VERSION/local-ai-$VERSION-$(uname -s | tr '[:upper:]' '[:lower:]')-$ARCH" \
&& chmod +x local-ai && ./local-ai
```

Otherwise, here are the links to the binaries:

| OS | Link |
| --- | --- |
| Linux (amd64) | [Download](https://github.com/mudler/LocalAI/releases/download/{{< version >}}/local-ai-Linux-x86_64) |
| Linux (arm64) | [Download](https://github.com/mudler/LocalAI/releases/download/{{< version >}}/local-ai-Linux-arm64) |
| MacOS (arm64) | [Download](https://github.com/mudler/LocalAI/releases/download/{{< version >}}/local-ai-Darwin-arm64) |
| Linux (amd64) | [Download](https://github.com/mudler/LocalAI/releases/download/{{< version >}}/local-ai-{{< version >}}-linux-amd64) |
| Linux (arm64) | [Download](https://github.com/mudler/LocalAI/releases/download/{{< version >}}/local-ai-{{< version >}}-linux-arm64) |
| MacOS (arm64) | [Download](https://github.com/mudler/LocalAI/releases/download/{{< version >}}/local-ai-{{< version >}}-darwin-arm64) |
| Windows (amd64) | [Download](https://github.com/mudler/LocalAI/releases/download/{{< version >}}/local-ai-{{< version >}}-windows-amd64.exe) |


{{% notice icon="⚡" context="warning" %}}
Expand All @@ -38,4 +41,5 @@ Binaries do have limited support compared to container images:
- Python-based backends are not shipped with binaries (e.g. `diffusers` or `transformers`)
- MacOS binaries and Linux-arm64 do not ship TTS nor `stablediffusion-cpp` backends
- Linux binaries do not ship `stablediffusion-cpp` backend
- The Windows binary ships only the `llama-cpp` backend (native, no WSL required); see the [Windows guide]({{% relref "getting-started/windows" %}}) for details
{{% /notice %}}
7 changes: 6 additions & 1 deletion pkg/downloader/auth_progress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"sync"

. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -141,7 +142,11 @@ var _ = Describe("authenticated HTTP downloads", func() {
Expect(errors.Is(err, context.Canceled)).To(BeTrue())
info, statErr := os.Stat(target + ".partial")
Expect(statErr).NotTo(HaveOccurred())
Expect(info.Mode().Perm()).To(Equal(os.FileMode(0o600)))
// POSIX permission bits are not enforceable on Windows, where the file
// is reported with the default 0666 mode regardless of chmod.
if runtime.GOOS != "windows" {
Expect(info.Mode().Perm()).To(Equal(os.FileMode(0o600)))
}
})

It("keeps the legacy total empty when the response length is unknown", func() {
Expand Down
7 changes: 7 additions & 0 deletions pkg/downloader/partial_resume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -157,6 +158,12 @@ var _ = Describe("DownloadFile with a leftover .partial", func() {
})

It("reports an informative error when the partial cannot be stat'd", func() {
if runtime.GOOS == "windows" {
// A self-referencing symlink is the portable trick that makes
// os.Stat fail with ELOOP, but creating symlinks on Windows needs
// admin rights or Developer Mode, so the setup itself errors first.
Skip("symlink creation requires privileges on Windows")
}
server := rangeServer(true, nil, nil)
defer server.Close()

Expand Down
11 changes: 11 additions & 0 deletions pkg/downloader/uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,10 @@ func (uri URI) DownloadFileWithContext(ctx context.Context, filePath, sha string

if startPos > 0 && resp.StatusCode != http.StatusPartialContent {
_ = resp.Body.Close()
// The origin ignored the resume range, so the partial is garbage;
// a retry must start clean. Drop the write handle first — Windows
// cannot delete a file that is still open (no FILE_SHARE_DELETE).
_ = outFile.Close()
_ = removePartialFile(tmpFilePath)
// The partial has just been discarded, so a further attempt starts
// clean and no longer needs the server to honour the range.
Expand Down Expand Up @@ -805,6 +809,13 @@ func (uri URI) DownloadFileWithContext(ctx context.Context, filePath, sha string
// after filesystem permissions while the disk was perfectly healthy.
tracked := &readErrorRecorder{r: source}
_, err = xio.Copy(ctx, io.MultiWriter(outFile, progress), tracked)
// Windows cannot rename or remove a file that still has an open handle:
// Go opens files without FILE_SHARE_DELETE, so MoveFileEx / DeleteFile fail
// with a sharing violation while outFile is live. The copy is the last use
// of the handle, so close it before any of the removal/rename paths below —
// the error paths (non-206 resume, user cancel, SHA mismatch) remove the
// partial too, and POSIX unlink works on open files while Windows does not.
_ = outFile.Close()
if err != nil {
// Detect cancellation via the context (a cause-cancelled read surfaces
// the cause, not context.Canceled). Keep the .partial for resume,
Expand Down
Loading