From e62cfc8660e32c3680110aab56dc0a64b3088e14 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Wed, 23 Sep 2026 16:27:07 -0700 Subject: [PATCH] p3: no Tavily daily cap for restricted callers Restricted searches skip the cap check. Credits land on tavily:restricted so they never raise the fleet row. Fleet at cap still falls through. --- docs/P3.md | 5 ++++- internal/search/engine.go | 22 ++++++++++++++----- internal/search/engine_test.go | 40 ++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/docs/P3.md b/docs/P3.md index dc8a273..8ac18c8 100644 --- a/docs/P3.md +++ b/docs/P3.md @@ -241,7 +241,10 @@ Digest/question-embedding keys are partitioned the same way in P2.6. **Daily cap** (paid vendors, currently Tavily): calendar UTC day, in-process counter plus a `search_cap` row in SQLite so a restart does not reset it. Hitting the cap skips that source for the rest of the day (`warnings: tavily: daily cap`) and does -not fail the search. Brave LLM Context gets a cap too if the key is billed; default +not fail the search. **`permit_restricted` callers skip the cap check.** Their +credits are stored on a separate row (`tavily:restricted`) so they never raise the +fleet row, and the fleet cap never throttles them. Per-caller `search_credits` +still counts both. Brave LLM Context gets a cap too if the key is billed; default off until the bake-off shows a rate. Logs: one INFO line `search caller=… q_len=… sources=… n=… cache=hit|miss rerank_ms=…`. diff --git a/internal/search/engine.go b/internal/search/engine.go index f4baf24..44ccd07 100644 --- a/internal/search/engine.go +++ b/internal/search/engine.go @@ -99,7 +99,7 @@ func (e *Engine) Run(ctx context.Context, query string, k int, doRerank bool, ca class = dcache.ClassFleet } - attempted := e.attemptedNames() + attempted := e.attemptedNames(class) key := dcache.SearchKey(query, attempted, k, e.now(), class) if b, ok := e.Cache.GetSearch(key); ok { var hits []Hit @@ -123,7 +123,7 @@ func (e *Engine) Run(ctx context.Context, query string, k int, doRerank bool, ca var wg sync.WaitGroup for i, src := range e.Sources { name := src.Name() - if name == NameTavily && e.tavilyCapped() { + if name == NameTavily && e.tavilyCapped(class) { outs[i] = one{name: name, out: SourceOut{Warn: "tavily: daily cap"}, skip: true} e.addCapped(NameTavily) continue @@ -164,7 +164,7 @@ func (e *Engine) Run(ctx context.Context, query string, k int, doRerank bool, ca if o.out.Credits > 0 { e.addCredits(caller, o.name, o.out.Credits) if o.name == NameTavily && e.Cache != nil { - e.Cache.AddSearchCredits(NameTavily, e.now().UTC().Format("2006-01-02"), o.out.Credits) + e.Cache.AddSearchCredits(tavilyCapSource(class), e.now().UTC().Format("2006-01-02"), o.out.Credits) } } } @@ -190,10 +190,10 @@ func (e *Engine) Run(ctx context.Context, query string, k int, doRerank bool, ca } } -func (e *Engine) attemptedNames() []string { +func (e *Engine) attemptedNames(class string) []string { var n []string for _, s := range e.Sources { - if s.Name() == NameTavily && e.tavilyCapped() { + if s.Name() == NameTavily && e.tavilyCapped(class) { continue } n = append(n, s.Name()) @@ -202,10 +202,20 @@ func (e *Engine) attemptedNames() []string { return n } -func (e *Engine) tavilyCapped() bool { +func tavilyCapSource(class string) string { + if class == dcache.ClassRestricted { + return NameTavily + ":restricted" + } + return NameTavily +} + +func (e *Engine) tavilyCapped(class string) bool { if e == nil || e.TavilyCap <= 0 { return false } + if class == dcache.ClassRestricted { + return false + } n := 0 if e.Cache != nil { n = e.Cache.GetSearchCredits(NameTavily, e.now().UTC().Format("2006-01-02")) diff --git a/internal/search/engine_test.go b/internal/search/engine_test.go index 4040f97..fbb5ad0 100644 --- a/internal/search/engine_test.go +++ b/internal/search/engine_test.go @@ -195,6 +195,46 @@ func TestDailyCapSkipsTavily(t *testing.T) { } } +func TestRestrictedSkipsTavilyCap(t *testing.T) { + db, err := dcache.Open(filepath.Join(t.TempDir(), "c.db"), 1<<20, nil) + if err != nil { + t.Fatal(err) + } + defer db.Close() + tav := fakeSrc{name: NameTavily, hits: []Hit{{URL: "https://t.example/a", Source: NameTavily}}, credits: 1} + e := &Engine{Sources: []Source{tav}, DefaultK: 8, Cache: db, TavilyCap: 1, RerankOn: false} + e.Run(context.Background(), "q1", 8, false, "titan", dcache.ClassFleet) + day := e.now().UTC().Format("2006-01-02") + if db.GetSearchCredits(NameTavily, day) != 1 { + t.Fatalf("fleet row %d", db.GetSearchCredits(NameTavily, day)) + } + r := e.Run(context.Background(), "q-gs", 8, false, "straylight-gs", dcache.ClassRestricted) + if len(r.Hits) != 1 || r.Hits[0].URL != "https://t.example/a" { + t.Fatalf("restricted should still get Tavily: %+v %v", r.Hits, r.Warnings) + } + for _, w := range r.Warnings { + if strings.Contains(w, "daily cap") { + t.Fatalf("restricted capped: %v", r.Warnings) + } + } + if db.GetSearchCredits(NameTavily, day) != 1 { + t.Fatalf("restricted must not raise fleet row, got %d", db.GetSearchCredits(NameTavily, day)) + } + if db.GetSearchCredits(NameTavily+":restricted", day) != 1 { + t.Fatalf("restricted row %d", db.GetSearchCredits(NameTavily+":restricted", day)) + } + fleet := e.Run(context.Background(), "q2", 8, false, "titan", dcache.ClassFleet) + n := 0 + for _, w := range fleet.Warnings { + if strings.Contains(w, "daily cap") { + n++ + } + } + if n != 1 { + t.Fatalf("fleet still capped: %v", fleet.Warnings) + } +} + func TestDedupeLongerSnippet(t *testing.T) { in := []Hit{ {URL: "https://Ex.com/a#x", Snippet: "aa", Source: NameSearxNG},