- Go 1.26 or later (uses
iter.Seq,maps,slices) - No CGO, no build tags, no external tools required
- The package compiles on macOS, Linux, and Windows without modification
# Run all tests
go test ./...
# Run a single test by name
go test -run TestDefault_Good_Metal ./...
# Vet for common mistakes
go vet ./...
# View test coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.outThere is no Taskfile in this package; it is small enough that direct go invocations suffice. The parent workspace (/Users/snider/Code/host-uk/core) uses Task for cross-repo operations.
This package is part of the host-uk/core Go workspace. After adding or changing module dependencies:
go work syncThe workspace root is /Users/snider/Code/host-uk/core. The workspace file (go.work) includes this module alongside cmd/core-gui, cmd/bugseti, and others.
dappco.re/go/core/inference
Import it in consumers:
import "dappco.re/go/core/inference"Remote: ssh://git@forge.lthn.ai:2223/core/go-inference.git (legacy — module path is now dappco.re/go/core/inference)
go-inference/
├── inference.go # TextModel, Backend, Token, Message, registry, LoadModel
├── options.go # GenerateConfig, LoadConfig, all With* options
├── discover.go # Discover() and DiscoveredModel
├── inference_test.go # Tests for registry, LoadModel, all types
├── options_test.go # Tests for GenerateConfig, LoadConfig, all options
├── discover_test.go # Tests for Discover()
├── go.mod
├── go.sum
├── CLAUDE.md # Agent instructions
├── README.md
└── docs/
├── architecture.md
├── development.md
└── history.md
Tests follow the _Good, _Bad, _Ugly suffix convention used across the Core Go ecosystem:
_Good— happy path; confirms the documented behaviour works correctly_Bad— expected error conditions; confirms errors are returned with useful messages_Ugly— edge cases, panics, surprising-but-valid behaviour (e.g. last-option-wins, registry overwrites)
func TestDefault_Good_Metal(t *testing.T) { ... }
func TestDefault_Bad_NoBackends(t *testing.T) { ... }
func TestDefault_Ugly_SkipsUnavailablePreferred(t *testing.T) { ... }Tests that touch the global backend registry call resetBackends(t) first. This helper clears the map and is defined in inference_test.go:
func resetBackends(t *testing.T) {
t.Helper()
backendsMu.Lock()
defer backendsMu.Unlock()
backends = map[string]Backend{}
}Because resetBackends is in the inference package (not inference_test), it has direct access to the unexported backends map. Tests must not rely on registration order across test functions; each test that uses the registry must call resetBackends at the top.
inference_test.go provides stubBackend and stubTextModel — minimal implementations of Backend and TextModel for use in registry and routing tests. These are in the inference package itself (not a separate _test package) to allow access to unexported fields.
When writing new tests, use the existing stubs rather than creating new ones unless you need behaviour the stubs do not support.
Prefer table-driven tests for options and configuration variants. The existing TestApplyGenerateOpts_Good, TestWithTemperature_Good, and TestDefault_Good_PriorityOrder tests demonstrate the pattern:
tests := []struct {
name string
val float32
want float32
}{
{"greedy", 0.0, 0.0},
{"low", 0.3, 0.3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := ApplyGenerateOpts([]GenerateOption{WithTemperature(tt.val)})
assert.InDelta(t, tt.want, cfg.Temperature, 0.0001)
})
}Use testify/assert and testify/require:
requirefor preconditions where failure makes subsequent assertions meaningless (e.g.require.NoError(t, err)before using the returned value)assertfor all other checksassert.InDeltafor float32/float64 comparisons (never==)
UK English throughout: colour, organisation, centre, licence (noun), serialise, recognise. American spellings are not accepted in comments, documentation, or error messages.
Standard gofmt formatting. No custom style rules. Run gofmt -w . or go fmt ./... before committing.
Error strings start with the package name and a colon, lowercase, no trailing period:
fmt.Errorf("inference: no backends registered (import a backend package)")
fmt.Errorf("inference: backend %q not registered", cfg.Backend)
fmt.Errorf("inference: backend %q not available on this hardware", cfg.Backend)This convention matches the Go standard library and makes errors.Is/errors.As wrapping straightforward.
All parameters and return types are explicitly typed. No interface{} or any outside of test helpers where unavoidable.
No new external dependencies may be added to the production code. The go.mod require block must remain stdlib-only for non-test code. testify is the only permitted test dependency.
If you find yourself wanting an external library, reconsider the approach. This package is intentionally minimal.
Every new .go file must carry the EUPL-1.2 licence header:
// Copyright (c) Lethean Technologies Ltd. All rights reserved.
// SPDX-License-Identifier: EUPL-1.2Existing files without this header will be updated in a future housekeeping pass.
Use conventional commits:
type(scope): short imperative description
Longer explanation if needed. UK English. Wrap at 72 characters.
Types: feat, fix, test, docs, refactor, chore
Scope: inference, options, discover, or omit for cross-cutting changes.
Examples:
feat(inference): add WithParallelSlots load option
fix(discover): handle config.json with invalid JSON gracefully
test(options): add table-driven tests for WithTopP
docs: expand architecture section on registry priority
Always include the co-author trailer:
Co-Authored-By: Virgil <virgil@lethean.io>
To implement a new backend (e.g. go-vulkan for cross-platform GPU inference):
- Import
dappco.re/go/core/inferencein the new module. - Implement
inference.Backend:
type vulkanBackend struct{}
func (b *vulkanBackend) Name() string { return "vulkan" }
func (b *vulkanBackend) Available() bool {
// Check whether Vulkan runtime is present on this host.
return vulkan.IsAvailable()
}
func (b *vulkanBackend) LoadModel(path string, opts ...inference.LoadOption) (inference.TextModel, error) {
cfg := inference.ApplyLoadOpts(opts)
// Load model using cfg.ContextLen, cfg.GPULayers, etc.
return &vulkanModel{...}, nil
}- Implement
inference.TextModel(all nine methods). - Register in
init(), guarded by the appropriate build tag:
//go:build linux && (amd64 || arm64)
func init() { inference.Register(&vulkanBackend{}) }- Write stub-based tests to confirm the backend registers and
LoadModelroutes correctly without requiring real GPU hardware in CI.
Before adding a method to TextModel or Backend, consider:
- Do two or more existing consumers require this capability right now?
- Can the capability be expressed as a separate interface that embeds
TextModel? - Will adding this method break existing backend implementations that do not yet provide it?
If the answer to the first question is no, defer the addition. If a separate interface is sufficient, prefer that approach. See docs/architecture.md for the stability contract.
When a new method is genuinely necessary, coordinate with the owners of go-mlx, go-rocm, and go-ml before merging, since all three must implement the new method simultaneously or the interface will be broken at build time.