fix(budget): price free local chat providers at $0, matching embed and rerank (#3541)

Co-Authored-By: Ben Young <Grimnoth@users.noreply.github.com>
This commit is contained in:
Garry Tan
2026-08-01 07:40:48 +08:00
committed by Sina Matian
co-authored by Ben Young
parent 37ad1d2104
commit 03de3246f3
2 changed files with 85 additions and 0 deletions
+27
View File
@@ -156,6 +156,27 @@ const FREE_LOCAL_EMBED_PROVIDERS: ReadonlySet<string> = new Set([
'llama-server',
]);
/**
* Chat sibling of FREE_LOCAL_EMBED_PROVIDERS / FREE_LOCAL_RERANK_PROVIDERS.
*
* Local inference costs electricity, not tokens, so these providers price at
* $0 rather than TX2 hard-failing. Without this a caller that sets ANY cost cap
* cannot use a local chat model at all: CANONICAL_PRICING has no `ollama:*`
* keys, so `reserve()` throws no_pricing before the first call and every work
* item is skipped with `budget_exhausted: true` at $0 spent.
*
* That is not theoretical — `cycle.extract_atoms` always constructs its tracker
* with `maxCostUsd` (config only accepts `n > 0`, so the cap can't be unset),
* which made `models.dream.extract_atoms: ollama:*` silently extract nothing.
*
* `litellm` is excluded on purpose, matching the embed set: a LiteLLM proxy can
* front a paid provider, so pricing-unknown is the honest state there.
*/
const FREE_LOCAL_CHAT_PROVIDERS: ReadonlySet<string> = new Set([
'ollama',
'llama-server',
]);
/**
* Look up `modelId` in the chat or embedding pricing maps. Returns a
* per-1M-token price tuple, or null when unknown.
@@ -220,6 +241,12 @@ function lookupPricing(modelId: string, kind: BudgetKind): ModelPricing | null {
// above is only the bare-keyed Claude view.
const canon = canonicalLookup(modelId);
if (canon) return canon;
// Local-inference chat providers cost electricity, not tokens. Checked AFTER
// the canonical table so an explicitly-priced local entry, should one ever be
// added, still wins over the blanket zero.
if (kind === 'chat' && providerId && FREE_LOCAL_CHAT_PROVIDERS.has(providerId)) {
return { input: 0, output: 0 };
}
return null;
}
@@ -0,0 +1,58 @@
import { describe, test, expect } from 'bun:test';
import { BudgetTracker, BudgetExhausted } from '../../src/core/budget/budget-tracker.ts';
/**
* Regression guard for a silent-zero-yield bug: `cycle.extract_atoms` always
* constructs its BudgetTracker with a cap (config only accepts `n > 0`, so it
* cannot be unset). Local chat models have no CANONICAL_PRICING entry, so TX2
* hard-failed `no_pricing` before the first call and every page was skipped
* with `budget_exhausted: true` at $0 spent — extraction reported success and
* produced nothing.
*/
const est = (modelId: string, kind: 'chat' | 'embed' | 'rerank' = 'chat') => ({
modelId, kind, estimatedInputTokens: 12_000, maxOutputTokens: 4096,
});
describe('free local chat providers under a cost cap', () => {
test('ollama chat reserves at $0 instead of hard-failing', () => {
const t = new BudgetTracker({ maxCostUsd: 0.3, label: 'test' });
expect(() => t.reserve(est('ollama:gemma4:26b'))).not.toThrow();
expect(t.totalSpent).toBe(0);
});
test('llama-server chat also reserves at $0', () => {
const t = new BudgetTracker({ maxCostUsd: 0.3, label: 'test' });
expect(() => t.reserve(est('llama-server:qwen3-32b'))).not.toThrow();
});
test('a genuinely unpriced remote provider still hard-fails (TX2 intact)', () => {
const t = new BudgetTracker({ maxCostUsd: 0.3, label: 'test' });
let err: unknown;
try { t.reserve(est('some-unknown-vendor:mystery-model')); } catch (e) { err = e; }
expect(err).toBeInstanceOf(BudgetExhausted);
expect((err as BudgetExhausted).reason).toBe('no_pricing');
});
test('litellm is NOT free — a proxy can front a paid provider', () => {
// Mirrors the embed set's deliberate exclusion. Pricing-unknown is the
// honest state for a proxy, so the cap must still hard-fail.
const t = new BudgetTracker({ maxCostUsd: 0.3, label: 'test' });
expect(() => t.reserve(est('litellm:gpt-5.4'))).toThrow(BudgetExhausted);
});
test('priced models are unaffected — real cost still projected', () => {
const t = new BudgetTracker({ maxCostUsd: 0.3, label: 'test' });
expect(() => t.reserve(est('anthropic:claude-haiku-4-5'))).not.toThrow();
// And a cap smaller than the projected cost still throws on cost, not pricing.
const tight = new BudgetTracker({ maxCostUsd: 0.000001, label: 'test' });
let err: unknown;
try { tight.reserve(est('anthropic:claude-opus-4-7')); } catch (e) { err = e; }
expect(err).toBeInstanceOf(BudgetExhausted);
expect((err as BudgetExhausted).reason).toBe('cost');
});
test('embed and rerank paths are untouched by the chat addition', () => {
const t = new BudgetTracker({ maxCostUsd: 0.3, label: 'test' });
expect(() => t.reserve(est('ollama:nomic-embed-text', 'embed'))).not.toThrow();
});
});