mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
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:
committed by
Sina Matian
co-authored by
Ben Young
parent
37ad1d2104
commit
03de3246f3
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user