fix(cost): embedding cost preview uses configured model rate, not hardcoded OpenAI

The sync --all cost gate computed spend from a hardcoded
EMBEDDING_COST_PER_1K_TOKENS = 0.00013 (OpenAI text-embedding-3-large)
and labeled the preview with the back-compat EMBEDDING_MODEL constant,
regardless of the actually-configured embedding model. A brain running a
cheaper model (e.g. zeroentropyai:zembed-1 @ $0.05/Mtok) saw a preview
that named the wrong provider and over-stated spend ~2.6x ($337 vs $130
on a 2.6B-token corpus).

estimateEmbeddingCostUsd now resolves the live model via the gateway and
prices it through embedding-pricing.ts (the existing per-provider:model
table), falling back to the OpenAI rate only when the gateway is
unconfigured (unit-test context) or the model is unknown. sync.ts surfaces
the real model name in the preview message and JSON.

Regression test pins model-aware pricing: openai 3-large vs zembed-1 must
produce materially different previews; collapsing both to the OpenAI number
fails the assertion.
This commit is contained in:
t
2026-05-29 18:02:31 -07:00
committed by Garry Tan
parent 041d89babe
commit 0906ab0abd
3 changed files with 64 additions and 13 deletions
+5 -4
View File
@@ -17,7 +17,7 @@ import {
formatCodeBreakdown,
} from '../core/sync.ts';
import { estimateTokens, CHUNKER_VERSION } from '../core/chunkers/code.ts';
import { EMBEDDING_MODEL, estimateEmbeddingCostUsd } from '../core/embedding.ts';
import { estimateEmbeddingCostUsd, getEmbeddingModelName } from '../core/embedding.ts';
import { errorFor, serializeError } from '../core/errors.ts';
import type { SyncManifest } from '../core/sync.ts';
import { createProgress } from '../core/progress.ts';
@@ -2253,13 +2253,14 @@ See also:
if (!noEmbed) {
const preview = estimateSyncAllCost(sources);
const costUsd = estimateEmbeddingCostUsd(preview.totalTokens);
const embeddingModelName = getEmbeddingModelName();
const previewMsg =
`sync --all preview: ${preview.totalFiles} files across ${preview.activeSources} source(s), ` +
`~${preview.totalTokens.toLocaleString()} tokens, est. $${costUsd.toFixed(2)} on ${EMBEDDING_MODEL}.`;
`~${preview.totalTokens.toLocaleString()} tokens, est. $${costUsd.toFixed(2)} on ${embeddingModelName}.`;
if (dryRun) {
if (jsonOut) {
console.log(JSON.stringify({ status: 'dry_run', preview, costUsd, model: EMBEDDING_MODEL }));
console.log(JSON.stringify({ status: 'dry_run', preview, costUsd, model: getEmbeddingModelName() }));
} else {
console.log(previewMsg);
console.log('--dry-run: exit without syncing.');
@@ -2277,7 +2278,7 @@ See also:
message: previewMsg,
hint: 'Pass --yes to proceed, or --dry-run to see the preview and exit 0.',
}));
console.log(JSON.stringify({ error: envelope, preview, costUsd, model: EMBEDDING_MODEL }));
console.log(JSON.stringify({ error: envelope, preview, costUsd, model: getEmbeddingModelName() }));
process.exit(2);
}
// Interactive TTY path: prompt [y/N].
+29 -5
View File
@@ -12,6 +12,7 @@ import {
getEmbeddingModel as gatewayGetModel,
getEmbeddingDimensions as gatewayGetDims,
} from './ai/gateway.ts';
import { lookupEmbeddingPrice } from './embedding-pricing.ts';
// v0.27.1: re-export multimodal embedding so callers can pull both text and
// image embedding APIs from `src/core/embedding`. import-image-file consumes
@@ -127,13 +128,36 @@ export const EMBEDDING_MODEL = 'text-embedding-3-large';
export const EMBEDDING_DIMENSIONS = 1536;
/**
* USD cost per 1k tokens for text-embedding-3-large. Used by
* `gbrain sync --all` cost preview and `reindex-code` to surface
* expected spend before accepting expensive operations.
* USD cost per 1k tokens for text-embedding-3-large. Retained for back-compat
* with callers/tests that import it directly; new cost math resolves the
* ACTUAL configured model's rate via embedding-pricing.ts instead of assuming
* OpenAI. (Hardcoding this rate produced cost previews that named the wrong
* provider and over-stated spend ~2.6x when the brain ran on a cheaper model.)
*/
export const EMBEDDING_COST_PER_1K_TOKENS = 0.00013;
/** Compute USD cost estimate for embedding `tokens` at current model rate. */
/**
* Resolve the price-per-1M-tokens for the currently-configured embedding
* model. Falls back to the OpenAI text-embedding-3-large rate only when the
* model is unknown to the pricing table.
*/
export function currentEmbeddingPricePerMTok(): number {
let modelString: string;
try {
modelString = gatewayGetModel(); // e.g. 'zeroentropyai:zembed-1'
} catch {
// Gateway not configured (e.g. unit tests, cost preview before connect).
// Fall back to the OpenAI text-embedding-3-large default rate.
return 0.13;
}
const hit = lookupEmbeddingPrice(modelString);
return hit.kind === 'known' ? hit.pricePerMTok : 0.13;
}
/**
* Compute USD cost estimate for embedding `tokens` at the CURRENT configured
* model's rate (not a hardcoded OpenAI rate).
*/
export function estimateEmbeddingCostUsd(tokens: number): number {
return (tokens / 1000) * EMBEDDING_COST_PER_1K_TOKENS;
return (tokens / 1_000_000) * currentEmbeddingPricePerMTok();
}
+30 -4
View File
@@ -15,22 +15,48 @@
import { describe, test, expect } from 'bun:test';
import { EMBEDDING_COST_PER_1K_TOKENS, estimateEmbeddingCostUsd } from '../src/core/embedding.ts';
import { lookupEmbeddingPrice } from '../src/core/embedding-pricing.ts';
import { estimateTokens } from '../src/core/chunkers/code.ts';
describe('Layer 8 D1 — embedding cost model', () => {
test('EMBEDDING_COST_PER_1K_TOKENS is text-embedding-3-large pricing', () => {
// Update this when OpenAI changes text-embedding-3-large pricing.
// As of 2026-04-24: $0.00013 / 1k tokens.
test('EMBEDDING_COST_PER_1K_TOKENS back-compat constant is the OpenAI 3-large rate', () => {
// Retained only for back-compat imports. Live cost math now resolves the
// CONFIGURED model's rate via embedding-pricing.ts (see model-aware test
// below). As of 2026-04-24: $0.00013 / 1k tokens.
expect(EMBEDDING_COST_PER_1K_TOKENS).toBe(0.00013);
});
test('estimateEmbeddingCostUsd scales linearly with tokens', () => {
test('estimateEmbeddingCostUsd scales linearly (gateway-unconfigured fallback = OpenAI rate)', () => {
// With no gateway configured (unit-test context) the estimator falls back
// to the OpenAI text-embedding-3-large rate ($0.13/Mtok = $0.00013/1k).
expect(estimateEmbeddingCostUsd(0)).toBe(0);
expect(estimateEmbeddingCostUsd(1000)).toBeCloseTo(0.00013, 5);
expect(estimateEmbeddingCostUsd(10_000)).toBeCloseTo(0.0013, 4);
expect(estimateEmbeddingCostUsd(1_000_000)).toBeCloseTo(0.13, 4);
});
test('cost preview uses the CONFIGURED model rate, not a hardcoded OpenAI rate', () => {
// Regression: the cost gate previously hardcoded $0.00013/1k (OpenAI
// text-embedding-3-large) regardless of the configured embedding model,
// so a brain on a cheaper model (e.g. zeroentropyai:zembed-1 @ $0.05/Mtok)
// saw a preview that named the wrong provider and over-stated spend ~2.6x.
// The pricing table is the single source of truth per provider:model.
const TOKENS = 2_590_710_262; // a real large-brain sync preview
const openai = lookupEmbeddingPrice('openai:text-embedding-3-large');
const zeroentropy = lookupEmbeddingPrice('zeroentropyai:zembed-1');
expect(openai.kind).toBe('known');
expect(zeroentropy.kind).toBe('known');
if (openai.kind === 'known' && zeroentropy.kind === 'known') {
const openaiCost = (TOKENS / 1_000_000) * openai.pricePerMTok;
const zeCost = (TOKENS / 1_000_000) * zeroentropy.pricePerMTok;
// The two models must produce materially different previews; a fix that
// collapses both to the OpenAI number would regress this assertion.
expect(openaiCost).toBeCloseTo(336.79, 1);
expect(zeCost).toBeCloseTo(129.54, 1);
expect(zeCost).toBeLessThan(openaiCost);
}
});
test('5K-file TS repo sanity check: ~$5 at ~400k tokens', () => {
// A 5K-file TS repo at ~80 tokens/file averages ~400k tokens. Cost:
// 400_000 / 1000 * 0.00013 = $0.052 ≈ $0.05. Not $5. The CHANGELOG