# AIKit: a Makefile pinned to v0.21.0, a Go 1.27 builder, and a spec dependency at 0.0.7

> AIKit wraps LocalAI and Unsloth into container images so an open model can be served or fine-tuned without a GPU. The build files are where the interesting facts live: three different version strings, a supply chain claim the Makefile only partly implements, and a platform matrix that misses an image sitting in the root.

**kaito-project/aikit** — 🏗️ Fine-tune, build, and deploy open-source LLMs easily!

- Repository: https://github.com/kaito-project/aikit
- Website: https://kaito-project.github.io/aikit/
- Stars: 539 · Forks: 57
- Language: Go
- License: MIT
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/kaito-project-aikit

## The Makefile pins v0.21.0 while the binary takes its version from git tags

The build file opens with `VERSION := v0.21.0`. The most recent published release is `v0.23.0` from 2026-09-18, and `v0.22.1` sits between them. So the constant in the build file is two releases behind the tags.

That constant is also not what ends up in the binary. The version string is injected at link time from git:

```makefile
GIT_COMMIT := $(shell git rev-list --abbrev-commit --tags --max-count=1)
GIT_TAG := $(shell git describe --abbrev=0 --tags ${GIT_COMMIT} 2>/dev/null || true)
LDFLAGS := "-X github.com/kaito-project/aikit/pkg/version.Version=$(GIT_TAG:%=%)"
```

Three forms of the same number, then. The Makefile variable carries a leading `v`, the git tag carries a leading `v`, and the substitution in LDFLAGS strips it, so the compiled binary reports a bare numeric string while every tag and release name is prefixed.

The `|| true` fallback is the part to watch. On a shallow clone, or on a tree with no tags reachable, `git describe` fails, the fallback yields an empty string, and the binary is stamped with an empty version with no build-time error. Nothing in the build stops that from shipping.

Two other defaults in the same block matter for reproduction: `PLATFORMS ?= linux/amd64,linux/arm64` and `KIND_VERSION ?= 0.29.0` with `KUBERNETES_VERSION ?= 1.33.2`.

## The module declares Go 1.26.8 and the builder image is Go 1.27

`go.mod` declares `go 1.26.8`. The Dockerfile builds with `FROM --platform=$BUILDPLATFORM golang:1.27-bookworm`, pinned by digest.

A language floor and a builder tag are different things, so this is not automatically wrong: a module can declare 1.26.8 and be compiled by a newer toolchain. What it does mean is that the effective compiler version is a property of the Dockerfile digest rather than of the module file, and the two are free to drift in either direction. When a build behaves differently from what the module declares, the Dockerfile is where the answer is.

The build itself is a static Go binary with CGO disabled, linked with `-w -s` and static external link flags, targeting `./cmd/frontend`. The whole repository is copied to a GOPATH-shaped location, `/go/src/github.com/kaito-project/aikit`, rather than using a module cache mount, so every build context includes the full tree.

The final stage is `FROM scratch`. The result copies two things out of the builder: the certificate bundle from `/etc/ssl/certs`, and the binary into `/bin/aikit` with an entrypoint of `/bin/aikit`. There is no shell, no package manager, and no configuration file in that image.

The dependency list explains why a Go project needs container plumbing at all: buildkit, containerd, the OCI image spec and digest packages, and in-toto attestation libraries. AIKit drives container image builds from Go, which is a different shape of tool from a model server.

## A scratch image with no USER instruction runs its binary as root

The security story in the feature list is built on two claims: a minimal image size producing fewer vulnerabilities and a smaller attack surface, and supply chain protection through SBOMs, provenance attestations, and signed images.

The minimal-size half is delivered literally. The runtime stage is `scratch`, so there is no shell to exec into, no package manager to install more, and no configuration to tamper with. For a service that binds a port and proxies model requests, that is a real reduction in what an attacker has available.

What the stage does not contain is a `USER` instruction. Nothing in the visible Dockerfile drops privileges, so the entrypoint runs as root inside the container by default. That is a narrower point than it sounds, since a scratch image has little for root to damage inside itself, but it is the difference between a container that can be given a read-only filesystem and a non-root user without a rebuild and one that cannot.

The other half of the security claim is where the build file disagrees with the feature list, which is the subject of the next section. For now, note that the certificate bundle is copied in by hand. That is the correct way to do it, and it is also the reason HTTPS works at all in a stage with no distribution tooling.

## The main image target asks for no SBOM and no provenance, and the test target disables provenance

Three build targets in the Makefile, three different supply chain settings.

`build-base` is the careful one. It builds from `Dockerfile.base`, and it passes `--sbom=true --push`, so an SBOM is generated and the result is pushed rather than left in the local daemon.

`build-test-model` does the opposite on provenance. It builds with `-f ${TEST_FILE}` where the default is `test/aikitfile-llama.yaml`, and it passes `--provenance=false` explicitly alongside `--progress=plain` and `--output=${OUTPUT_TYPE}`. Turning provenance off for a test image is defensible, since these are throwaway artifacts built on every change.

`build-aikit` is the one that matters most and the one that says nothing. It builds the main frontend image with `--output`, `--build-arg LDFLAGS`, `--platform` and `--progress=plain`. There is no `--sbom`, no `--provenance`, and no `--push`.

So the feature bullet promising SBOMs, provenance attestations, and signed images is implemented for the base image, explicitly waived for test images, and simply not requested for the image people actually run. Whether the release pipeline adds those flags elsewhere is not visible here, which is exactly the thing to check before treating a pulled AIKit image as attested.

The module dependencies suggest the capability exists in the code: in-toto attestation and securesystemslib are both present, alongside buildkit.

## CNCF ModelPack support is pinned to a 0.0.7 specification

OCI packaging is one of the three headline capabilities, and the description says it supports the CNCF ModelPack specification alongside generic artifact packaging.

The dependency that implements it is `github.com/modelpack/model-spec v0.0.7`. That is a pre-1.0 version, and a project version of 0.0.x is conventionally read as an interface that has not settled. So the packaging format AIKit emits follows a specification that is still declaring itself unstable.

This is worth knowing before you build a pipeline around the output rather than before you use the tool. AIKit producing artefacts that a registry accepts is a different question from a third party pinning their tooling to the same 0.0.x contract, because that contract can move without a major version bump.

The rest of the dependency set is ordinary container work. The OCI image spec and digest packages handle references and content addressing, containerd platform packages handle multi-architecture selection, and `gopkg.in/yaml.v2` is the parser behind the declarative configuration for inference and fine-tuning that the feature list promises. Note the yaml.v2 line specifically: the project pins the v2 series rather than the v3 series, and the two have different marshalling behaviour.

The repository layout follows the same split. `cmd/`, `internal/`, and `pkg/` hold the Go code, `runners/` and `models/` hold the runtime and model definitions, `charts/` holds the Kubernetes packaging, `website/` holds the documentation site, and `test/` holds the aikitfile used by the test build.

## An Apple Silicon base image sits in the root that the default platform matrix never builds

The root holds four Dockerfiles: the main `Dockerfile`, `Dockerfile.base`, `Dockerfile.base-applesilicon`, and `Dockerfile.hf-cli`.

The default platform list in the Makefile is `PLATFORMS ?= linux/amd64,linux/arm64`. Both entries are Linux. There is no `darwin` entry anywhere in that variable, and the build targets all pass `--platform ${PLATFORMS}`.

So the Apple Silicon base image is not produced by the default build. It exists as a file, and it is reachable only if you invoke a target or a buildx invocation that targets Darwin yourself. The feature bullet claims support for AMD64 and ARM64 CPUs and adds that Docker will automatically pull the correct image for your CPU, and the CPU note repeats that the same command runs on either architecture with the most optimised instruction set selected automatically.

For an Apple Silicon machine, that promise resolves through the ARM64 Linux image rather than through the `applesilicon` base, unless you build it deliberately. The two paths are not interchangeable and the documentation does not say which one you get.

The GPU story has the same shape. There are separate run targets for plain CPU, for NVIDIA with `--gpus all`, and for ROCm, and the ROCm one passes `--device /dev/kfd --device /dev/dri --group-add video` plus a `stat -c` call for the render group. That is Linux-specific syntax, so the ROCm path has no macOS equivalent in the build file at all.

## The container tag and the model name you send to the API are different strings

The quick start pulls one image and then calls the API with a different identifier.

```bash
curl http://localhost:8080/v1/chat/completions -H "Content-Type: application/json" -d '{
    "model": "qwen-3.5-4b",
    "messages": [{"role": "user", "content": "explain kubernetes in a sentence"}]
  }'
```

The image is `ghcr.io/kaito-project/aikit/qwen3.5:4b`. The model string in the request is `qwen-3.5-4b`. The pre-made model table confirms the pattern is systematic rather than a typo: the image tag `gemma4:e2b` corresponds to the model name `gemma-4-e2b-instruct`, and `llama3.2:3b` corresponds to `llama-3.2-3b-instruct`.

The OpenAI-compatible `model` field therefore takes a server-side identifier that does not match the container tag you pulled. A client that derives the model name from the image reference by default will send something the server does not recognise.

The same table is also where the licensing gets interesting. The Qwen 3.5 and 3.8 entries, Gemma 4 E2B, and Devstral Small 2 are all under Apache 2.0, with the Gemma row pointing at Google's own licence page rather than the generic chooser. The Llama 3.2 entries are under the Llama licence instead. So one repository, one image naming scheme, and at least three different terms covering the models it will pull for you, on top of the repository's own MIT licence.

## No internet needed, once you have already pulled the image and the weights

The first feature bullet says no GPU, no internet access, and no additional tools are needed except Docker or Podman. The last bullet qualifies the internet claim in a way that deserves to be read first.

Air-gapped inference is supported with self-hosted or local registries when model content and dependencies are baked or mirrored ahead of time. Runner images that download models at startup are not air-gapped by default.

That sentence defines the boundary. The quick start is a `docker run` against `ghcr.io`, which requires a registry pull. If the image then downloads weights at startup, that is a second network dependency inside a container you may have started believing was offline.

The quick start also passes `--rm`, which removes the container when it exits. Combined with a startup download, that means the weight cache lives in the container filesystem and goes away with it, so the next start pays the download again. That is fine for a first look and expensive as a service restart policy.

The mitigation the project itself names is baking or mirroring content ahead of time and pointing at a self-hosted or local registry, which is what the packaging capability and the Helm charts under `charts/` exist to support. It is a deliberate workflow rather than a default.

One more boundary is worth naming. The pre-made model table in the visible documentation stops partway through the Llama 3.2 3B row, inside that licence URL, so the rows past it are not present in what you can read here.

## Conclusion

AIKit is a reasonable way to get an OpenAI-compatible endpoint on a laptop CPU, and the declarative specs plus the Helm charts under charts/ are the parts worth studying even if you never run an image. Do not adopt the supply chain claims at face value from the feature list, because the main image target in the Makefile asks for no SBOM and no provenance while the test target explicitly disables provenance. Before you pin anything, check whether you want the git tag version rather than the VERSION variable, decide how you will handle model weights across restarts given the quick start uses `--rm`, and confirm the container tag and the OpenAI model name you pass to the API are the same string, because they are not.

## FAQ

### What does AIKit actually run?

Inference goes through LocalAI, which provides a drop-in OpenAI-compatible REST API, and fine-tuning goes through Unsloth. Models are distributed as OCI artefacts, and the documentation states that no GPU, internet access or extra tooling is needed beyond Docker or Podman.

### How do I run an AIKit model without a GPU?

The quick start is a single command: `docker run -d --rm -p 8080:8080 ghcr.io/kaito-project/aikit/qwen3.5:4b`, after which the WebUI is at http://localhost:8080/chat. Both AMD64 and ARM64 CPUs are supported and the most optimised instruction set is selected automatically.

### Is AIKit compatible with the OpenAI API?

Yes, through LocalAI's OpenAI-compatible REST layer, so any OpenAI-compatible client can talk to it. The API example posts to http://localhost:8080/v1/chat/completions with a model name of qwen-3.5-4b.

### What version of AIKit is current?

The most recent release is v0.23.0 from 2026-09-18, after v0.22.1 and v0.21.0. The Makefile still declares VERSION as v0.21.0, while the binary version is injected at link time from the nearest git tag with the leading v stripped.

### Can AIKit run in an air-gapped environment?

Yes, with self-hosted or local registries when model content and dependencies are baked or mirrored ahead of time. Runner images that download models at startup are stated not to be air-gapped by default.

### What licences apply to the models AIKit ships images for?

They differ per model. The Qwen 3.5 and 3.8 entries, Gemma 4 E2B and Devstral Small 2 are listed under Apache 2.0, while the Llama 3.2 entries are listed under the Llama licence. The repository itself is MIT licensed.

## Sources

- [kaito-project/aikit on GitHub](https://github.com/kaito-project/aikit)
- [License: MIT](https://github.com/kaito-project/aikit/blob/main/LICENSE)
- [Project website](https://kaito-project.github.io/aikit/)
- [README](https://github.com/kaito-project/aikit/blob/main/README.md)
- [Releases](https://github.com/kaito-project/aikit/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/kaito-project-aikit
