WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
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
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
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user