Files
xinmi/internal/models/embedding/request_budget_test.go
T
yawan f7fc992f57
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

258 lines
9.4 KiB
Go

package embedding
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/Tencent/WeKnora/internal/models/requestbudget"
"github.com/Tencent/WeKnora/internal/types"
"github.com/panjf2000/ants/v2"
)
type embeddingBudgetGuard struct {
mu sync.Mutex
limit int
models, phases []string
}
func (g *embeddingBudgetGuard) Key() string { return "local-embedding-test" }
func (g *embeddingBudgetGuard) Check(ctx context.Context) error { return ctx.Err() }
func (g *embeddingBudgetGuard) Reserve(ctx context.Context, model, phase string) error {
g.mu.Lock()
defer g.mu.Unlock()
if err := ctx.Err(); err != nil {
return err
}
if len(g.models) >= g.limit {
return requestbudget.ErrStopped
}
g.models, g.phases = append(g.models, model), append(g.phases, phase)
return nil
}
func embeddingBudgetContext(g *embeddingBudgetGuard) context.Context {
return requestbudget.WithModel(requestbudget.WithPhase(requestbudget.WithGuard(context.Background(), g), "wiki_embedding"), "caller-forged-model")
}
func assertEmbeddingBudget(t *testing.T, g *embeddingBudgetGuard, sends int) {
t.Helper()
g.mu.Lock()
defer g.mu.Unlock()
if len(g.models) != sends {
t.Fatalf("sends=%d reservations=%d", sends, len(g.models))
}
for i := range g.models {
if g.models[i] != "concrete-embedding" || g.phases[i] != "wiki_embedding" {
t.Fatalf("lost adapter identity/context: %v %v", g.models, g.phases)
}
}
}
func TestRequestBudgetEmbeddingProviderTransports(t *testing.T) {
t.Setenv("SSRF_WHITELIST", "127.0.0.1")
var sends atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sends.Add(1)
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"data":[{"index":0,"embedding":[0.1]}]}`)
}))
defer server.Close()
client := newEmbeddingHTTPClient(3 * time.Second)
calls := map[string]func(context.Context, []byte) (*http.Response, error){
"openai": (&OpenAIEmbedder{baseURL: server.URL, modelID: "concrete-embedding", httpClient: client}).doRequestWithRetry,
"azure": (&AzureOpenAIEmbedder{baseURL: server.URL, modelID: "concrete-embedding", httpClient: client}).doRequestWithRetry,
"aliyun": (&AliyunEmbedder{baseURL: server.URL, modelID: "concrete-embedding", httpClient: client}).doRequestWithRetry,
"volcengine": (&VolcengineEmbedder{baseURL: server.URL, modelID: "concrete-embedding", httpClient: client}).doRequestWithRetry,
"jina": (&JinaEmbedder{baseURL: server.URL, modelID: "concrete-embedding", httpClient: client}).doRequestWithRetry,
"nvidia": (&NvidiaEmbedder{baseURL: server.URL, modelID: "concrete-embedding", httpClient: client}).doRequestWithRetry,
"gemini": (&GeminiEmbedder{baseURL: server.URL, modelID: "concrete-embedding", httpClient: client}).doRequestWithRetry,
"zhipu": (&ZhipuEmbedder{baseURL: server.URL, modelID: "concrete-embedding", httpClient: client}).doRequestWithRetry,
}
for name, call := range calls {
for _, limit := range []int{0, 1} {
t.Run(fmt.Sprintf("%s/limit=%d", name, limit), func(t *testing.T) {
sends.Store(0)
g := &embeddingBudgetGuard{limit: limit}
resp, err := call(embeddingBudgetContext(g), []byte(`{}`))
if resp != nil {
resp.Body.Close()
}
if limit == 0 && !errors.Is(err, requestbudget.ErrStopped) {
t.Fatalf("guard not applied: %v", err)
}
if limit == 1 && err != nil {
t.Fatal(err)
}
if int(sends.Load()) != limit {
t.Fatalf("sends=%d want=%d", sends.Load(), limit)
}
assertEmbeddingBudget(t, g, limit)
})
}
}
for _, limit := range []int{0, 1} {
t.Run(fmt.Sprintf("weknoracloud/limit=%d", limit), func(t *testing.T) {
sends.Store(0)
e := &WeKnoraCloudEmbedder{baseURL: server.URL, modelName: "local-test", modelID: "concrete-embedding", client: client}
g := &embeddingBudgetGuard{limit: limit}
_, err := e.BatchEmbed(embeddingBudgetContext(g), []string{"local test"})
if limit == 0 && !errors.Is(err, requestbudget.ErrStopped) {
t.Fatalf("guard not applied: %v", err)
}
if limit == 1 && err != nil {
t.Fatal(err)
}
if int(sends.Load()) != limit {
t.Fatalf("sends=%d want=%d", sends.Load(), limit)
}
assertEmbeddingBudget(t, g, limit)
})
}
}
func TestRequestBudgetEmbeddingPoolCountsEachBatch(t *testing.T) {
t.Setenv("SSRF_WHITELIST", "127.0.0.1")
t.Setenv("BATCH_EMBED_SIZE", "2")
for _, limit := range []int{0, 2, 4} {
t.Run(fmt.Sprint(limit), func(t *testing.T) {
var sends atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sends.Add(1)
var body struct {
Input []string `json:"input"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
data := make([]map[string]any, len(body.Input))
for i := range body.Input {
data[i] = map[string]any{"index": i, "embedding": []float32{0.1}}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"data": data})
}))
defer server.Close()
pool, err := ants.NewPool(3)
if err != nil {
t.Fatal(err)
}
defer pool.Release()
e, err := NewEmbedder(Config{Source: types.ModelSourceRemote, Provider: "openai", BaseURL: server.URL, APIKey: "local-only", ModelID: "concrete-embedding", ModelName: "local-test"}, NewBatchEmbedder(pool), nil)
if err != nil {
t.Fatal(err)
}
g := &embeddingBudgetGuard{limit: limit}
ctx, cancel := context.WithTimeout(embeddingBudgetContext(g), 3*time.Second)
defer cancel()
vectors, err := e.BatchEmbedWithPool(ctx, e, []string{"a", "b", "c", "d", "e", "f", "g"})
if limit < 4 && !errors.Is(err, requestbudget.ErrStopped) {
t.Fatalf("batch escaped budget: %v", err)
}
if limit == 4 && (err != nil || len(vectors) != 7) {
t.Fatalf("batch failed: %d %v", len(vectors), err)
}
if int(sends.Load()) != limit {
t.Fatalf("batch sends=%d want=%d", sends.Load(), limit)
}
assertEmbeddingBudget(t, g, limit)
})
}
}
func TestRequestBudgetEmbeddingRetriesConsumeReservations(t *testing.T) {
t.Setenv("SSRF_WHITELIST", "127.0.0.1")
for _, limit := range []int{1, 2} {
t.Run(fmt.Sprint(limit), func(t *testing.T) {
var sends atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if sends.Add(1) == 1 {
connection, _, err := w.(http.Hijacker).Hijack()
if err == nil {
connection.Close()
}
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"data":[{"index":0,"embedding":[0.1]}]}`)
}))
defer server.Close()
e, err := NewOpenAIEmbedder("local-only", server.URL, "local-test", 0, 0, "concrete-embedding", nil)
if err != nil {
t.Fatal(err)
}
e.maxRetries = 1
g := &embeddingBudgetGuard{limit: limit}
_, err = e.BatchEmbed(embeddingBudgetContext(g), []string{"test"})
if limit == 1 && !errors.Is(err, requestbudget.ErrStopped) {
t.Fatalf("retry escaped budget: %v", err)
}
if limit == 2 && err != nil {
t.Fatal(err)
}
if int(sends.Load()) != limit {
t.Fatalf("retry sends=%d want=%d", sends.Load(), limit)
}
assertEmbeddingBudget(t, g, limit)
})
}
}
func TestRequestBudgetVolcengineCountsEachText(t *testing.T) {
t.Setenv("SSRF_WHITELIST", "127.0.0.1")
var sends atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sends.Add(1)
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"data":{"embedding":[0.1]}}`)
}))
defer server.Close()
e := &VolcengineEmbedder{baseURL: server.URL, modelID: "concrete-embedding", modelName: "local-test", httpClient: newEmbeddingHTTPClient(time.Second)}
g := &embeddingBudgetGuard{limit: 2}
_, err := e.BatchEmbed(embeddingBudgetContext(g), []string{"a", "b", "c"})
if !errors.Is(err, requestbudget.ErrStopped) || sends.Load() != 2 {
t.Fatalf("per-text budget not enforced: sends=%d err=%v", sends.Load(), err)
}
assertEmbeddingBudget(t, g, 2)
}
func TestRequestBudgetEmbeddingFactoryFailsClosed(t *testing.T) {
g := &embeddingBudgetGuard{limit: 1}
ctx := embeddingBudgetContext(g)
e, err := NewEmbedder(Config{Source: types.ModelSourceLocal, ModelID: "local-model"}, nil, nil)
if err != nil {
t.Fatal(err)
}
if _, err := e.Embed(ctx, "test"); !errors.Is(err, requestbudget.ErrStopped) {
t.Fatalf("local embed invoked: %v", err)
}
if _, err := e.BatchEmbed(ctx, []string{"test"}); !errors.Is(err, requestbudget.ErrStopped) {
t.Fatalf("local batch invoked: %v", err)
}
if _, err := e.BatchEmbedWithPool(ctx, e, []string{"test"}); !errors.Is(err, requestbudget.ErrStopped) {
t.Fatalf("local pool invoked: %v", err)
}
assertEmbeddingBudget(t, g, 0)
fake := newFakeEmbedder("unknown")
close(fake.release)
unknown := wrapEmbeddingRequestBudget(fake)
if _, err := unknown.Embed(ctx, "test"); !errors.Is(err, requestbudget.ErrStopped) {
t.Fatalf("unknown embed adapter allowed: %v", err)
}
if _, err := unknown.BatchEmbed(ctx, []string{"test"}); !errors.Is(err, requestbudget.ErrStopped) {
t.Fatalf("unknown batch adapter allowed: %v", err)
}
if _, err := unknown.BatchEmbedWithPool(ctx, unknown, []string{"test"}); !errors.Is(err, requestbudget.ErrStopped) {
t.Fatalf("unknown pool adapter allowed: %v", err)
}
if len(fake.enter) != 0 {
t.Fatal("unsupported adapter was invoked")
}
}