JuliusBrussee/caveman · error · Error
invalid chars-per-token ratio
Error message
invalid chars-per-token ratio: ${ratio} What it means
estimateTokens() converts a character count to an approximate token count using a chars-per-token ratio that must be strictly positive (the library calibrates 5.9–6.9 against provider-exact Claude Code prefixes; default 6.4). A ratio of 0, negative, NaN, or a non-numeric value would produce Infinity/NaN token counts, so it is rejected with this error.
Solutions
- Pass a positive number for ratio (omit the argument to use DEFAULT_CHARS_PER_TOKEN = 6.4).
- Coerce config values with Number(value) and check Number.isFinite(ratio) && ratio > 0 before calling.
- Fix the upstream computation producing NaN/0 (e.g. guard division by zero when deriving a measured chars/token ratio).
- Check argument order — the signature is estimateTokens(chars, ratio); ensure you are not passing a char count as the ratio.
Example fix
// before const ratio = Number(process.env.CHARS_PER_TOKEN); // NaN when unset estimateTokens(chars, ratio); // throws // after const raw = Number(process.env.CHARS_PER_TOKEN); const ratio = Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_CHARS_PER_TOKEN; estimateTokens(chars, ratio);
Defensive patterns
Strategy: validation
Validate before calling
function validRatio(v) {
const n = typeof v === "number" ? v : Number(v);
return Number.isFinite(n) && n > 0 ? n : DEFAULT_CHARS_PER_TOKEN;
}
estimateTokens(chars, validRatio(configuredRatio)); Type guard
const isPositiveNumber = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
Try / catch
try {
return estimateTokens(chars, ratio);
} catch (e) {
if (e.message.startsWith("invalid chars-per-token ratio")) {
return estimateTokens(chars, DEFAULT_CHARS_PER_TOKEN); // fall back to calibrated default
} else throw e;
} Prevention
- Validate env/config numeric values with Number() and Number.isFinite before use.
- Never pass a computed ratio without guarding the division that produced it (avoid 0/NaN).
- Keep the ratio within the calibrated 5.9–6.9 band; omit the argument to use the 6.4 default.
When it happens
Trigger: Calling estimateTokens(chars, ratio) with ratio = 0, a negative number, NaN (e.g. from a failed parseFloat of config), undefined chained wrongly, or a string like "6.4" that fails the > 0 numeric coercion check.
Common situations: Loading the ratio from config/env where the value failed to parse into a number; dividing to compute a measured ratio and getting 0 or NaN when the denominator was 0; typo passing chars and ratio arguments swapped.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- aes cipher
- apiKey, baseURL, and agent are required
- ASGI context resolver or request bounds are invalid
- AutoGen tools and workbench are mutually exclusive
- awssig: signer requires region and service
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/789469b8371f34d3.
Report an issue: GitHub.
Appendix: source
Thrown at packages/subagent-tax/lib/tokens.mjs:15
// Token accounting. Two rungs, never blended:
// - "est": chars / ratio. Calibrated against provider-exact Claude Code runs
// on 2026-08-07 (observed 5.9–6.9 chars/token on full harness
// prefixes; 6.4 is the midpoint). One machine's calibration — a
// labeled estimate, not a measurement.
// - "exact": Anthropic's count_tokens endpoint (a free, separate rate bucket),
// opt-in via --count-tokens + ANTHROPIC_API_KEY, and only for
// anthropic-protocol captures (other tokenizers differ).
export const DEFAULT_CHARS_PER_TOKEN = 6.4;
export const CALIBRATION_NOTE =
"chars/token calibrated 5.9–6.9 against provider-exact Claude Code prefixes (2026-08-07, one machine); 6.4 = midpoint";
export function estimateTokens(chars, ratio = DEFAULT_CHARS_PER_TOKEN) {
if (!(ratio > 0)) throw new Error(`invalid chars-per-token ratio: ${ratio}`);
return { tokens: Math.round(chars / ratio), basis: "est", ratio };
}
// Rebuild a count_tokens request from a captured anthropic-messages body,
// passing the captured fields through verbatim so the count is of what the
// harness actually sent.
export function buildCountTokensRequest(capturedBody) {
const { model, system, tools, messages } = capturedBody;
if (!model || !Array.isArray(messages)) return null;
const req = { model, messages };
if (system !== undefined) req.system = system;
if (Array.isArray(tools) && tools.length > 0) req.tools = tools;
return req;
}
export async function countTokensAnthropic(capturedBody, { apiKey, baseUrl = "https://api.anthropic.com", fetchImpl = fetch } = {}) {
const req = buildCountTokensRequest(capturedBody);
if (!req) return { error: "capture is not a countable anthropic-messages body" };View on GitHub (pinned to 3ee70a1026)