package llm import ( "context" "errors" "sync" ) // FakeClient 是测试用 LLMClient,可编程产出固定 stream 事件。 // 注意:本文件 NOT _test.go 后缀,因下游 integration test 需作为依赖导入。 type FakeClient struct { mu sync.Mutex ProviderID string Events []StreamEvent // 流事件序列(按顺序 emit) StreamError error // Stream() 立即返回的错误(如果非 nil) TokenCount int // CountTokens 返回值 StreamCalls int // BlockCh, if non-nil, causes Stream goroutine to block after emitting all // Events until ctx is cancelled or BlockCh is closed. Useful for cancel tests. BlockCh chan struct{} } // NewFake 构造一个 FakeClient,默认 provider="fake"。 func NewFake() *FakeClient { return &FakeClient{ProviderID: "fake"} } func (f *FakeClient) Provider() string { return f.ProviderID } func (f *FakeClient) Stream(ctx context.Context, modelID string, req Request) (<-chan StreamEvent, error) { f.mu.Lock() f.StreamCalls++ if f.StreamError != nil { err := f.StreamError f.mu.Unlock() return nil, err } events := append([]StreamEvent(nil), f.Events...) f.mu.Unlock() blockCh := f.BlockCh ch := make(chan StreamEvent, len(events)+1) go func() { defer close(ch) for _, e := range events { select { case <-ctx.Done(): ch <- StreamEvent{Type: EventError, Err: errors.New("context canceled")} return case ch <- e: } } // If a block channel is set, pause here until ctx is cancelled or BlockCh // is closed. This lets cancel tests confirm mid-stream cancellation. if blockCh != nil { select { case <-ctx.Done(): case <-blockCh: } } }() return ch, nil } func (f *FakeClient) CountTokens(ctx context.Context, modelID string, req Request) (int, error) { return f.TokenCount, nil } // FakeEmbedder is a deterministic Embedder for tests. Each input maps to a // fixed-length vector derived from its bytes, so identical text always yields the // same vector (lets tests assert idempotency / ranking without a real endpoint). type FakeEmbedder struct { Dims int // output dimension; defaults to PlatformEmbeddingDims when 0 Err error // if non-nil, Embed returns it mu sync.Mutex Calls int Inputs []string // accumulated inputs across calls (for assertions) } // NewFakeEmbedder constructs a FakeEmbedder with the platform dimension. func NewFakeEmbedder() *FakeEmbedder { return &FakeEmbedder{Dims: PlatformEmbeddingDims} } var _ Embedder = (*FakeEmbedder)(nil) func (e *FakeEmbedder) dims() int { if e.Dims > 0 { return e.Dims } return PlatformEmbeddingDims } // EmbedDims returns the configured dimension. func (e *FakeEmbedder) EmbedDims(string) int { return e.dims() } // Embed returns one deterministic vector per input. The vector is a normalized // projection of a small hash of the input across the dimension, so distinct // inputs are distinguishable and identical inputs are identical. func (e *FakeEmbedder) Embed(_ context.Context, _ string, inputs []string) ([][]float32, error) { e.mu.Lock() e.Calls++ e.Inputs = append(e.Inputs, inputs...) err := e.Err e.mu.Unlock() if err != nil { return nil, err } d := e.dims() out := make([][]float32, len(inputs)) for i, s := range inputs { v := make([]float32, d) // Seed from a stable rolling hash of the bytes. var h uint32 = 2166136261 for _, b := range []byte(s) { h ^= uint32(b) h *= 16777619 } for j := 0; j < d; j++ { h ^= uint32(j) * 2654435761 h *= 16777619 // Map to [-1,1). v[j] = float32(int32(h)) / float32(1<<31) } out[i] = v } return out, nil }