feat(llm): client interface and fake test double

This commit is contained in:
2026-06-07 13:05:59 +01:00
parent 9a4d329224
commit 43aa78abc5
3 changed files with 96 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
// Package llm defines a provider-agnostic LLM client for vision and audio
// transcription, with concrete HTTP implementations per provider.
package llm
import "context"
// Image is a decoded image to send to a vision model.
type Image struct {
MediaType string // e.g. "image/png", "image/jpeg"
Data []byte
}
// Options are per-call overrides. Zero values mean "use the client default".
type Options struct {
Model string
Language string
Temperature float64
MaxTokens int
}
// Client is the abstraction every provider implements and every handler depends on.
type Client interface {
// Vision sends a prompt plus images and returns the model's text answer.
Vision(ctx context.Context, prompt string, images []Image, opts Options) (string, error)
// Transcribe converts audio bytes to text. filename carries the extension
// for providers that infer the audio format from it. Returns ErrUnsupported
// if the provider has no audio capability.
Transcribe(ctx context.Context, audio []byte, filename string, opts Options) (string, error)
}
// ErrUnsupported is returned by Transcribe on providers without audio support.
type ErrUnsupported struct{ Provider string }
func (e ErrUnsupported) Error() string {
return "audio transcription unsupported by provider " + e.Provider
}
+39
View File
@@ -0,0 +1,39 @@
package llm
import "context"
// Fake is a deterministic Client for tests.
type Fake struct {
VisionResp string
TranscribeResp string
VisionErr error
TranscribeErr error
VisionCalls []VisionCall
TranscribeCalls []TranscribeCall
}
// VisionCall records a Vision invocation.
type VisionCall struct {
Prompt string
Opts Options
Images []Image
}
// TranscribeCall records a Transcribe invocation.
type TranscribeCall struct {
Filename string
Opts Options
Audio []byte
}
// Vision implements Client.
func (f *Fake) Vision(_ context.Context, prompt string, images []Image, opts Options) (string, error) {
f.VisionCalls = append(f.VisionCalls, VisionCall{Prompt: prompt, Images: images, Opts: opts})
return f.VisionResp, f.VisionErr
}
// Transcribe implements Client.
func (f *Fake) Transcribe(_ context.Context, audio []byte, filename string, opts Options) (string, error) {
f.TranscribeCalls = append(f.TranscribeCalls, TranscribeCall{Audio: audio, Filename: filename, Opts: opts})
return f.TranscribeResp, f.TranscribeErr
}
+21
View File
@@ -0,0 +1,21 @@
package llm
import (
"context"
"testing"
)
func TestFakeClient(t *testing.T) {
f := &Fake{VisionResp: "hello", TranscribeResp: "spoken"}
got, err := f.Vision(context.Background(), "p", []Image{{MediaType: "image/png", Data: []byte{1}}}, Options{})
if err != nil || got != "hello" {
t.Fatalf("vision got %q err %v", got, err)
}
if len(f.VisionCalls) != 1 || f.VisionCalls[0].Prompt != "p" {
t.Fatalf("vision call not recorded: %+v", f.VisionCalls)
}
tr, err := f.Transcribe(context.Background(), []byte{1}, "audio.mp3", Options{Language: "en"})
if err != nil || tr != "spoken" {
t.Fatalf("transcribe got %q err %v", tr, err)
}
}