.agents/coding-style.md
The project has the following .editorconfig:
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.go]
indent_style = tab
[Makefile]
indent_style = tab
[*.proto]
indent_size = 2
[*.py]
indent_size = 4
[*.js]
indent_size = 2
[*.yaml]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
any not interface{}Use github.com/mudler/xlog for logging which has the same API as slog.
All Go tests — including backend tests — must use Ginkgo (v2) with Gomega matchers, not the stdlib testing package with t.Run / t.Errorf. A test file should register a suite with RegisterFailHandler(Fail) in a TestXxx(t *testing.T) bootstrap and use Describe/Context/It blocks for the actual cases. Look at any existing *_test.go under core/ or pkg/ for a template.
Do not mix styles within a package. If you are extending tests in a package that already uses Ginkgo, keep using Ginkgo. If you find stdlib-style Go tests in the tree, treat them as tech debt to be migrated rather than as a pattern to follow.
This is enforced by golangci-lint via the forbidigo linter (see .golangci.yml); calls like t.Errorf / t.Fatalf / t.Run / t.Skip / t.Logf are flagged. Run make lint locally before submitting; the same check runs in CI (.github/workflows/lint.yml).
All outbound HTTP must go through github.com/mudler/LocalAI/pkg/httpclient rather than the standard library's default client. Use httpclient.New(...) (no body deadline — safe for streaming/SSE) or httpclient.NewWithTimeout(d, ...) (simple request/response). Both refuse redirects by default and set a TLS 1.2 floor.
The reason is GHSA-3mj3-57v2-4636: the std default client follows redirects, and on a cross-host redirect Go forwards custom credential headers (e.g. Anthropic's x-api-key) to the redirect target, leaking the secret. httpclient fails closed instead.
httpclient.WithFollowRedirects() — it still strips credential headers on any cross-host hop.RoundTripper)? Pass httpclient.WithTransport(rt), basing the transport on httpclient.HardenedTransport() to keep the TLS floor. Handed a *http.Client by a library? httpclient.Harden(c) applies the policy in place.This is enforced by forbidigo (see .golangci.yml): http.DefaultClient and http.Get/Post/PostForm/Head are flagged. The &http.Client{} composite literal can't be matched precisely by forbidigo without also flagging legitimate *http.Client type references, so that form is caught by review — don't construct raw clients.
The project documentation is located in docs/content. When adding new features or changing existing functionality, it is crucial to update the documentation to reflect these changes. This helps users understand how to use the new capabilities and ensures the documentation stays relevant.
docs/content/ in the SAME change, not as a follow-up. A user-facing change without a matching docs update is incomplete. The PR template carries a checklist item for this.docs/content/features/ explaining what it is, how to configure it, and how to use it.docs/content/.{{% notice note %}}, {{% notice tip %}}, or {{% notice warning %}} for callout boxes. Do not use {{% alert %}} — that shortcode does not exist in this project's Hugo theme and will break the docs build.The React UI ships a design system in core/http/react-ui/src/App.css: design
tokens, form grids, data tables, stat cards, callouts, plus a small semantic
primitive layer (.stack, .hstack, .text-note, .text-meta, .tone-*,
.icon-chip). Use it instead of style={{ ... }}. Inline styles are a
spacing or colour decision made in one file, so no two pages end up sharing a
rhythm, which is the main reason the app reads as unfinished.
Inline styles are still correct for values that are genuinely computed at
runtime: width: ${pct}%, a data-driven background, a tooltip's coordinates.
Everything else belongs in a class.
A ratchet enforces this:
cd core/http/react-ui
npm run lint:inline-styles # fails if the count went UP
npm run lint:inline-styles:report # per-file counts, worst first
npm run lint:inline-styles:write # refresh the baseline after converting
The gate also fails on duplicate className attributes on one element. JSX
keeps the last and silently drops the first, so <i className={icon} className="text-xs" /> loses its icon while passing lint, the build and the e2e
suite. Converting a style to a class on an element that already has a
className is the usual way to introduce one; merge them into a single
attribute instead.
When converting a page, prefer naming the shapes it actually has
(.p2p-diagram, .usage-tile) over adding more utilities, and check whether an
existing block already covers it: the Nodes page reuses the P2P setup shapes,
and Model Editor reuses the Settings section rail.