From 2f4ad2c0a415287f477b8e4ba0a43be959c623f6 Mon Sep 17 00:00:00 2001 From: Masa <98894508+Masashi-Ono0611@users.noreply.github.com> Date: Fri, 24 Jul 2026 06:22:01 +0900 Subject: [PATCH] fix(pricing): add the zeroentropyai:zerank-2 reranker entry the budget tracker needs (#3223) (#3233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zerank-2 is the default reranker under search_mode: tokenmax, but had no pricing entry — any --max-cost-capped rerank call TX2 hard-failed in BudgetTracker.reserve() with "no pricing entry". Adding the entry to EMBEDDING_PRICING alone (the issue's suggested fix) does not resolve this: lookupPricing()'s rerank branch in budget-tracker.ts never consulted that table at all, only ANTHROPIC_PRICING and the FREE_LOCAL_RERANK_PROVIDERS zero-price set. Verified by reproducing the hard-fail with only the pricing-table entry added and confirming it still threw. Fix: add the $0.025/1M-token entry (docs/ai-providers/zeroentropy.md) and wire the rerank branch to fall back to lookupEmbeddingPrice, reusing the existing provider:model-keyed table instead of duplicating a third pricing surface. Addresses the report in #3223. Co-authored-by: Claude Fable 5 --- src/core/budget/budget-tracker.ts | 18 +++++++++++--- src/core/embedding-pricing.ts | 4 ++++ test/core/budget/budget-tracker.test.ts | 31 +++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/core/budget/budget-tracker.ts b/src/core/budget/budget-tracker.ts index d5bf44e21..fd59d8213 100644 --- a/src/core/budget/budget-tracker.ts +++ b/src/core/budget/budget-tracker.ts @@ -166,9 +166,13 @@ const FREE_LOCAL_EMBED_PROVIDERS: ReadonlySet = new Set([ * local-inference providers (FREE_LOCAL_EMBED_PROVIDERS) price at $0 so * `--max-cost` callers don't hard-fail. * - Rerank: try ANTHROPIC_PRICING (legacy path for any Claude-priced - * rerank); else if the provider half is in FREE_LOCAL_RERANK_PROVIDERS, - * return zero pricing so `--max-cost` callers don't TX2 hard-fail on - * local inference recipes (electricity, not tokens); else unknown. + * rerank); else try lookupEmbeddingPrice — paid rerank providers (e.g. + * ZeroEntropy's zerank-2) share the same provider:model-keyed, + * $/1M-token table as their embedding siblings, so it's reused here + * rather than duplicated into a third table; else if the provider half + * is in FREE_LOCAL_RERANK_PROVIDERS, return zero pricing so `--max-cost` + * callers don't TX2 hard-fail on local inference recipes (electricity, + * not tokens); else unknown. */ function lookupPricing(modelId: string, kind: BudgetKind): ModelPricing | null { if (kind === 'embed') { @@ -194,6 +198,14 @@ function lookupPricing(modelId: string, kind: BudgetKind): ModelPricing | null { const tailHit = ANTHROPIC_PRICING[modelTail]; if (tailHit) return tailHit; } + // Paid rerank providers (e.g. ZeroEntropy's zerank-2) aren't Claude-priced, + // so they miss the ANTHROPIC_PRICING checks above. Reuse the embedding + // pricing table (issue #3223) — same provider:model key shape, same + // $/1M-token unit — instead of hand-copying a third pricing surface. + if (kind === 'rerank') { + const hit = lookupEmbeddingPrice(modelId); + if (hit.kind === 'known') return { input: hit.pricePerMTok, output: 0 }; + } // v0.40.6.1: zero-price local-inference rerank providers so the budget // tracker's TX2 hard-fail doesn't trip on `llama-server-reranker:` // under `--max-cost`. Only the rerank kind — chat/embed already have diff --git a/src/core/embedding-pricing.ts b/src/core/embedding-pricing.ts index 18774727d..e2b3450fb 100644 --- a/src/core/embedding-pricing.ts +++ b/src/core/embedding-pricing.ts @@ -37,6 +37,10 @@ export const EMBEDDING_PRICING: Record = { 'voyage:voyage-4-large': { pricePerMTok: 0.18 }, // ZeroEntropy (https://zeroentropy.dev/pricing — zembed-1) 'zeroentropyai:zembed-1': { pricePerMTok: 0.05 }, + // ZeroEntropy reranker (docs/ai-providers/zeroentropy.md — $0.025/1M tokens). + // Reused here (not a separate rerank table) because budget-tracker.ts's + // rerank-kind lookup falls back to this same table for paid providers. + 'zeroentropyai:zerank-2': { pricePerMTok: 0.025 }, // Mistral (https://mistral.ai/pricing/api/, verified 2026-07-19) 'mistral:mistral-embed': { pricePerMTok: 0.10 }, 'mistral:mistral-embed-2312': { pricePerMTok: 0.10 }, diff --git a/test/core/budget/budget-tracker.test.ts b/test/core/budget/budget-tracker.test.ts index 9dd236bb4..163c3256f 100644 --- a/test/core/budget/budget-tracker.test.ts +++ b/test/core/budget/budget-tracker.test.ts @@ -248,6 +248,37 @@ describe('BudgetTracker.reserve', () => { expect((caught as BudgetExhausted).reason).toBe('no_pricing'); }); + test('#3223: rerank kind for zeroentropyai:zerank-2 prices from the embedding table (no TX2 throw under --max-cost)', () => { + // Pre-fix: `search_mode: tokenmax` defaults the zerank-2 reranker ON + // (docs/ai-providers/zeroentropy.md), but lookupPricing's rerank branch + // never consulted the embedding pricing table (where ZeroEntropy's + // provider:model-keyed prices live) — so any --max-cost run that + // reranked TX2 hard-failed with "no pricing entry" even after adding + // the entry to EMBEDDING_PRICING alone. Fixed by wiring the rerank + // branch to fall back to lookupEmbeddingPrice. + const t = new BudgetTracker({ maxCostUsd: 0.0001, label: 'test', auditPath }); + expect(() => + t.reserve({ + modelId: 'zeroentropyai:zerank-2', + estimatedInputTokens: 3000, + maxOutputTokens: 0, + kind: 'rerank', + }), + ).not.toThrow(); + expect(t.totalSpent).toBe(0); // reserve() only projects; record() below banks it. + expect(() => + t.record({ + modelId: 'zeroentropyai:zerank-2', + inputTokens: 3000, + outputTokens: 0, + kind: 'rerank', + }), + ).not.toThrow(); + // $0.025/1M * 3000 tokens = $0.000075, under the $0.0001 cap — proves the + // real ZeroEntropy price was used, not a $0 fallback. + expect(t.totalSpent).toBeCloseTo(0.000075, 9); + }); + test('v0.40.x: local embed providers price at $0 (no TX2 throw under --max-cost)', () => { // FREE_LOCAL_EMBED_PROVIDERS — ollama / llama-server run on local inference // (electricity, not tokens). Pre-fix a --max-cost embed/reindex job