@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
117 lines (116 loc) • 5.05 kB
JavaScript
/**
* Decision limits — the one place a provider's `decisionLimits` are read and
* a state's size is estimated against them.
*
* `SystemOneDecisionProvider` refuses an over-limit request through these
* functions, and `NeuroLink.decisionLimits()` / `estimateDecisionStateTokens`
* expose the same functions to hosts, so what a host measures before the call
* is exactly what the pre-flight check measures inside it. Two estimators that
* drift apart would let a host size a state as fitting and then watch it be
* refused.
*
* @module utils/decisionLimits
*/
import { CHARS_PER_TOKEN, estimateTokens, serializeForEstimate, } from "./tokenEstimation.js";
/**
* Estimate how many tokens a decision model will read from `state`.
*
* `estimateTokens` assumes ~4 characters per token. That holds for English
* and is several times too generous for other scripts on an English
* tokenizer, so when the limits declare a `nonAsciiTokensPerChar` rate,
* non-ASCII characters are counted at it instead. Digits, ASCII punctuation
* and astral characters (emoji) are each counted separately only when the
* limits declare a rate for that class — a tokenizer that reads each digit,
* and most punctuation, on its own makes a number-heavy or JSON-heavy state
* several times longer than the ~4-characters-per-token estimate suggests. A
* non-string state is serialized first, as it is on the wire.
*
* This is the estimator the pre-flight refusal uses: a state this reports at
* or under `maxStateTokens` is sent, one over it is refused before any
* network call.
*/
export function estimateDecisionStateTokens(state, limits) {
const text = serializeForEstimate(state);
const nonAsciiTokensPerChar = limits?.nonAsciiTokensPerChar;
const digit = limits?.digitTokensPerChar;
const symbol = limits?.symbolTokensPerChar;
const astral = limits?.astralTokensPerChar;
if (nonAsciiTokensPerChar === undefined &&
digit === undefined &&
symbol === undefined &&
astral === undefined) {
return estimateTokens(text);
}
// Each class of character is counted once, at its own rate. A class with no
// declared rate stays in the ordinary four-characters-a-token estimate (or,
// for non-ASCII, at that estimate's per-character rate), so a provider that
// declares none of the extra rates is estimated exactly as before.
const isDigit = (c) => c >= "0" && c <= "9";
const isSymbol = (c) => /[!-/:-@[-`{-~]/.test(c);
let digits = 0;
let symbols = 0;
let astrals = 0;
let nonAscii = 0;
const rest = [];
for (const c of text) {
const code = c.codePointAt(0) ?? 0;
if (code > 0xffff && astral !== undefined) {
astrals += 1;
}
else if (code > 0x7f) {
nonAscii += 1;
}
else if (digit !== undefined && isDigit(c)) {
digits += 1;
}
else if (symbol !== undefined && isSymbol(c)) {
symbols += 1;
}
else {
rest.push(c);
}
}
return (estimateTokens(rest.join("")) +
Math.ceil(digits * (digit ?? 0)) +
Math.ceil(symbols * (symbol ?? 0)) +
Math.ceil(astrals * (astral ?? 0)) +
Math.ceil(nonAscii * (nonAsciiTokensPerChar ?? 1 / CHARS_PER_TOKEN)));
}
/**
* Flatten a descriptor's `decisionLimits` for one model: the per-model entry
* overrides the base, field by field, and `advisory` becomes
* `enforcedLocally: false`. Returns null for a descriptor that declares no
* limits.
*/
export function resolveDecisionLimitsReading(descriptor, model) {
const limits = descriptor.decisionLimits;
if (!limits) {
return null;
}
const modelLimits = limits.models?.[model];
const nonAsciiTokensPerChar = modelLimits?.nonAsciiTokensPerChar ?? limits.nonAsciiTokensPerChar;
return {
provider: descriptor.name,
model,
maxStateTokens: modelLimits?.maxStateTokens ?? limits.maxStateTokens,
// Absent for a provider that caps by tokens or bytes rather than by
// question count (TypeSafe, XOR).
...(limits.maxQuestions !== undefined
? { maxQuestions: limits.maxQuestions }
: {}),
...(nonAsciiTokensPerChar !== undefined ? { nonAsciiTokensPerChar } : {}),
// Not model-overridable — `models` only carries `maxStateTokens` and
// `nonAsciiTokensPerChar` — so these three come from the base limits only.
...(limits.digitTokensPerChar !== undefined
? { digitTokensPerChar: limits.digitTokensPerChar }
: {}),
...(limits.symbolTokensPerChar !== undefined
? { symbolTokensPerChar: limits.symbolTokensPerChar }
: {}),
...(limits.astralTokensPerChar !== undefined
? { astralTokensPerChar: limits.astralTokensPerChar }
: {}),
...(limits.media !== undefined ? { media: limits.media } : {}),
enforcedLocally: limits.advisory !== true,
};
}