feat: musicgen + embeddings/rerank surfaces (ADR-0021, ADR-0022)
- NEW musicgen leaf package: blocking Generate over ACE-Step's async job queue (release_task -> poll query_result -> fetch file, all via /upstream); tolerant envelope parsing, double-encoded result handled - NEW embeddings leaf package: EmbedModel + RerankModel as separate mints (two server instances on the host, llama.cpp #20085); InstructedQuery helper for Qwen3-style query/document asymmetry - provider/llamaswap: /v1/embeddings + /v1/rerank clients with strict validation (index-ordered vectors, count mismatch and out-of-range index are hard errors; rerank sorted descending, minimal parser) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// Package embeddings is majordomo's canonical text-embedding and reranking
|
||||
// surface (ADR-0022, following the ADR-0016→0021 leaf-contract lineage).
|
||||
// Two small halves, split like audio's Speech/Transcription so backends can
|
||||
// implement either:
|
||||
//
|
||||
// - EmbedModel turns texts into dense vectors (/v1/embeddings-style).
|
||||
// - RerankModel scores documents against a query with a cross-encoder
|
||||
// (/v1/rerank-style) — usually a DIFFERENT backend model than the
|
||||
// embedder, hence a separate mint.
|
||||
//
|
||||
// Instruction-aware embedders (Qwen3-Embedding et al.) want queries wrapped
|
||||
// as "Instruct: {task}\nQuery: {query}" while documents go in bare;
|
||||
// InstructedQuery encodes that so callers don't hand-roll (and silently
|
||||
// degrade retrieval) at each site.
|
||||
package embeddings
|
||||
|
||||
import "context"
|
||||
|
||||
// EmbedRequest is a batch embedding request. Inputs are embedded
|
||||
// independently; the result vector order matches the input order.
|
||||
type EmbedRequest struct {
|
||||
// Inputs are the texts to embed. Required (at least one).
|
||||
Inputs []string
|
||||
}
|
||||
|
||||
// EmbedResult is the canonical embedding result.
|
||||
type EmbedResult struct {
|
||||
// Vectors holds one embedding per input, in input order. Backends
|
||||
// normalize per their own convention (llama-server: Euclidean-normalized).
|
||||
Vectors [][]float32
|
||||
|
||||
// Raw is the provider-native response object. May be nil.
|
||||
Raw any
|
||||
}
|
||||
|
||||
// EmbedOption mutates an EmbedRequest before it is sent. Reserved: the
|
||||
// request shape is deliberately minimal today.
|
||||
type EmbedOption func(*EmbedRequest)
|
||||
|
||||
// Apply returns a copy of the request with all options applied.
|
||||
func (r EmbedRequest) Apply(opts ...EmbedOption) EmbedRequest {
|
||||
for _, opt := range opts {
|
||||
opt(&r)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// InstructedQuery wraps a retrieval QUERY for instruction-aware embedding
|
||||
// models. Documents must NOT be wrapped — the asymmetry is the point, and
|
||||
// getting it wrong silently costs retrieval quality. An empty task uses the
|
||||
// generic web-search instruction the reference model was trained with.
|
||||
func InstructedQuery(task, query string) string {
|
||||
if task == "" {
|
||||
task = "Given a web search query, retrieve relevant passages that answer the query"
|
||||
}
|
||||
return "Instruct: " + task + "\nQuery: " + query
|
||||
}
|
||||
|
||||
// EmbedModel embeds texts as dense vectors.
|
||||
type EmbedModel interface {
|
||||
// Embed returns one vector per input, in input order.
|
||||
Embed(ctx context.Context, req EmbedRequest, opts ...EmbedOption) (*EmbedResult, error)
|
||||
}
|
||||
|
||||
// EmbedModelOption configures an EmbedModel at construction time. Reserved
|
||||
// for future per-model settings.
|
||||
type EmbedModelOption func(*EmbedModelConfig)
|
||||
|
||||
// EmbedModelConfig carries per-model construction settings.
|
||||
type EmbedModelConfig struct{}
|
||||
|
||||
// ApplyEmbedModelOptions folds options into a config.
|
||||
func ApplyEmbedModelOptions(opts []EmbedModelOption) EmbedModelConfig {
|
||||
var cfg EmbedModelConfig
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// EmbedProvider mints embedding models bound to one backend.
|
||||
type EmbedProvider interface {
|
||||
// Name is the registry identifier for the provider.
|
||||
Name() string
|
||||
|
||||
// EmbedModel returns an EmbedModel bound to the given id (passed through
|
||||
// to the backend verbatim; no catalog validation).
|
||||
EmbedModel(id string, opts ...EmbedModelOption) (EmbedModel, error)
|
||||
}
|
||||
|
||||
// RerankRequest scores documents against a query.
|
||||
type RerankRequest struct {
|
||||
// Query is the search query. Required.
|
||||
Query string
|
||||
|
||||
// Documents are the candidate texts to score. Required (at least one).
|
||||
Documents []string
|
||||
|
||||
// TopN limits how many results the backend returns; 0 = all.
|
||||
TopN int
|
||||
}
|
||||
|
||||
// RerankItem is one scored document.
|
||||
type RerankItem struct {
|
||||
// Index is the document's position in the request's Documents slice.
|
||||
Index int
|
||||
|
||||
// Score is the backend's relevance score (higher = more relevant).
|
||||
// Scales are model-specific — compare within one response only.
|
||||
Score float64
|
||||
}
|
||||
|
||||
// RerankResult is the canonical rerank result, sorted by descending Score.
|
||||
type RerankResult struct {
|
||||
// Results are the scored documents (top-N when the request bounded it).
|
||||
Results []RerankItem
|
||||
|
||||
// Raw is the provider-native response object. May be nil.
|
||||
Raw any
|
||||
}
|
||||
|
||||
// RerankOption mutates a RerankRequest before it is sent.
|
||||
type RerankOption func(*RerankRequest)
|
||||
|
||||
// WithTopN limits how many results the backend returns.
|
||||
func WithTopN(n int) RerankOption { return func(r *RerankRequest) { r.TopN = n } }
|
||||
|
||||
// Apply returns a copy of the request with all options applied.
|
||||
func (r RerankRequest) Apply(opts ...RerankOption) RerankRequest {
|
||||
for _, opt := range opts {
|
||||
opt(&r)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// RerankModel scores documents against a query with a cross-encoder.
|
||||
type RerankModel interface {
|
||||
// Rerank returns scored documents sorted by descending relevance.
|
||||
Rerank(ctx context.Context, req RerankRequest, opts ...RerankOption) (*RerankResult, error)
|
||||
}
|
||||
|
||||
// RerankModelOption configures a RerankModel at construction time. Reserved
|
||||
// for future per-model settings.
|
||||
type RerankModelOption func(*RerankModelConfig)
|
||||
|
||||
// RerankModelConfig carries per-model construction settings.
|
||||
type RerankModelConfig struct{}
|
||||
|
||||
// ApplyRerankModelOptions folds options into a config.
|
||||
func ApplyRerankModelOptions(opts []RerankModelOption) RerankModelConfig {
|
||||
var cfg RerankModelConfig
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// RerankProvider mints rerank models bound to one backend.
|
||||
type RerankProvider interface {
|
||||
// Name is the registry identifier for the provider.
|
||||
Name() string
|
||||
|
||||
// RerankModel returns a RerankModel bound to the given id (passed
|
||||
// through to the backend verbatim; no catalog validation).
|
||||
RerankModel(id string, opts ...RerankModelOption) (RerankModel, error)
|
||||
}
|
||||
Reference in New Issue
Block a user