Files
zeekay 6f13ef421a pkg/miner: pluggable inference-engine backend interface
Close three `// TODO: Integrate with actual ...` stubs in
pkg/miner/miner.go (runInference, runChat, runEmbedding) by introducing
a small pluggable backend interface. The miner now dispatches through
backend.InferenceBackend rather than inlining placeholder output.

Two reference backends ship in-tree:

- pkg/miner/backend/noop  — deterministic mock, byte-for-byte identical
  to the pre-refactor inline stubs. Default. Zero config, zero deps.
- pkg/miner/backend/openai — OpenAI-compatible HTTP adapter built on
  net/http. Covers llama.cpp, vllm, ollama, LocalAI, and api.openai.com
  via OPENAI_API_BASE. No new go.mod dependencies.

Config gains Backend / OpenAIBase / OpenAIAPIKey / OpenAIModel /
OpenAIEmbeddingModel fields. Unknown backend values fall back to noop so
operator typos don't crash-loop the miner. Existing miner_test.go passes
unchanged; DefaultConfig() behaviour is preserved.

Also wire a GPUStatsProvider hook on Miner so GetStats can surface
best-effort GPU telemetry (GPUUtilization, MemoryUsed) without pulling
NVML/MLX bindings into the main binary. Default is nil = no-op, matching
current behaviour; a follow-up PR will wire a real provider.

Closes luxfi/ai#1.

Contributed by kcolbchain (https://kcolbchain.com) · https://abhishekkrishna.com
2026-04-21 06:55:21 +05:30

73 lines
2.0 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package backend_test
import (
"context"
"testing"
"github.com/luxfi/ai/pkg/miner/backend"
"github.com/luxfi/ai/pkg/miner/backend/noop"
)
// TestInterfaceContract ensures the canonical in-tree backends implement
// backend.InferenceBackend. If this test stops compiling, a backend has
// drifted from the interface.
func TestInterfaceContract(t *testing.T) {
var _ backend.InferenceBackend = noop.New()
}
// TestCapabilitiesShape exercises the Capabilities reporting path end-to-end
// for a concrete backend. The point is to lock in the JSON-visible shape;
// any rename breaks the wire contract for API consumers.
func TestCapabilitiesShape(t *testing.T) {
caps := noop.New().Capabilities()
if !caps.Chat || !caps.Inference || !caps.Embedding {
t.Fatalf("noop backend should advertise all three capabilities: %+v", caps)
}
if caps.EmbeddingDims == 0 {
t.Fatal("noop backend should advertise a fixed embedding dimensionality")
}
}
// TestRequestResponseRoundTrip sanity-checks that every request/response pair
// travels through the interface with no surprise mutations.
func TestRequestResponseRoundTrip(t *testing.T) {
ctx := context.Background()
b := noop.New()
chat, err := b.Chat(ctx, backend.ChatRequest{
Model: "test-model",
Messages: []backend.Message{{Role: "user", Content: "hi"}},
})
if err != nil {
t.Fatalf("Chat: %v", err)
}
if chat.Model != "test-model" {
t.Errorf("Chat: model not preserved, got %q", chat.Model)
}
inf, err := b.Inference(ctx, backend.InferenceRequest{
Model: "inf-model",
Prompt: "hello",
})
if err != nil {
t.Fatalf("Inference: %v", err)
}
if inf.Model != "inf-model" {
t.Errorf("Inference: model not preserved, got %q", inf.Model)
}
emb, err := b.Embed(ctx, backend.EmbedRequest{
Model: "emb-model",
Text: "some text",
})
if err != nil {
t.Fatalf("Embed: %v", err)
}
if emb.Model != "emb-model" {
t.Errorf("Embed: model not preserved, got %q", emb.Model)
}
}