Files
OmniRoute/src/lib/quota/tokenEstimator.ts
Benson K B 2d50ec0789 feat(routing): add quota-aware provider scheduling — Phase 2 (#10126)
* feat(quota): Phase 2 adapters, reset timers, analytics, and dashboard API

* feat(routing): add quota-aware provider scheduling (opt-in)

* fix(db): rename migration to 148_provider_quota_state.sql

* fix(quota): harden quota state route, isolate phase2 tests, slim env diff

- route: requireManagementAuth + Zod body validation + buildErrorBody
  sanitization (Hard Rule #12); fix clearProviderQuotaState -> clearProviderQuota
- .env.example/ENVIRONMENT.md: drop ~20 foreign vars, keep only
  OMNIROUTE_QUOTA_AWARE_ROUTING (migration 148)
- tests/unit/quota-phase2.test.ts: DATA_DIR mkdtemp + resetDbInstance teardown

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(ci): fix docs-sync + eslint-suppression drift for quota branch

CI gates flagged on PR #10126 head 43335f07:
- migration counts in README/AGENTS/llm.txt were stale (145 -> 146)
- regenerate docs/reference/PROVIDER_REFERENCE.md (gen-provider-reference)
- sync root llm.txt body into all 42 i18n mirrors (headers preserved)
- prune eslint suppressions that no longer occur

--no-verify: pre-commit docs-sync was failing on a pre-existing
release-base artifact (changelog 3.8.49 vs package 3.8.50) — fixed by
the changelog entry in the prior commit; re-verify in CI.

* chore(skills): regenerate agent skills (add omni-settings)

Merge-integrity CI gate flagged a missing generated skill. Regenerated
with check:agent-skills-sync --apply: +omni-settings, 45 unchanged.

* fix(ci): resolve Fast Quality Gates regressions on quota branch

- check-migration-numbering: migration 148 (provider_quota_state) landed
  on this branch, so the KNOWN_GAPS allowlist entry is stale — remove it
  (stale-enforcement 6A.3: 'REMOVA a entrada')
- open-sse/utils/stream.ts: duplicate sseCommentsEnabled import from a
  bad merge (lines 31 + 77) — TS2300 duplicate identifier; drop the
  duplicate so the open-sse typecheck gate is back within baseline

* docs: sync migration count to 149 after release merge

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* test(migrations): align 148 gap assertion after 148_provider_quota_state.sql landed

The phase-2 branch added 148_provider_quota_state.sql, and 148 was already
removed from KNOWN_GAPS in scripts/check/check-migration-numbering.mjs. The
frozen-allowlists assertion still expected 148 to be a gap, so it failed.
Flip the assertion to match the allowlist (same pattern as 143/147).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: benzntech <benzntech@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-08-18 10:49:19 -03:00

98 lines
3.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* tokenEstimator.ts — cheap, deterministic request token-cost estimation.
*
* Estimates the token cost of a chat request (input + reserved output) so
* the quota scheduler can decide whether a connection has budget before
* dispatching. Not a model — a heuristic:
* - chars / 4 approximates tokens for most latin text (OpenAI's classic
* heuristic); CJK and code skew higher, so the estimate is a floor.
* - max_tokens / max_completion_tokens reserves the output budget when
* present; otherwise a small default output allowance is used.
*
* The estimate deliberately OVER-provisions input (×1.1) so an exhausted
* budget is not misjudged as available. Errors never throw — a broken
* estimate degrades to "unknown cost" (scheduler treats as affordable).
*/
export interface TokenCostEstimate {
/** estimated input tokens (may be 0 when body is unparseable) */
inputTokens: number;
/** reserved output budget (max_tokens or default) */
outputTokens: number;
/** input + output */
totalTokens: number;
}
const DEFAULT_OUTPUT_ALLOWANCE = 1024;
const CHARS_PER_TOKEN = 4;
const OVER_PROVISION = 1.1;
/** Count a string's tokens by chars/4 (floor). */
export function estimateStringTokens(text: string): number {
if (!text) return 0;
return Math.ceil(text.length / CHARS_PER_TOKEN);
}
/**
* Estimate the token cost of an OpenAI-style chat body.
* Accepts both `messages` (chat.completions) and `input` (Responses API).
*/
export function estimateChatTokenCost(
body: Record<string, unknown> | null | undefined
): TokenCostEstimate {
if (!body || typeof body !== "object") {
return {
inputTokens: 0,
outputTokens: DEFAULT_OUTPUT_ALLOWANCE,
totalTokens: DEFAULT_OUTPUT_ALLOWANCE,
};
}
let inputTokens = 0;
const messages = body.messages;
if (Array.isArray(messages)) {
for (const msg of messages) {
if (!msg || typeof msg !== "object") continue;
const content = (msg as Record<string, unknown>).content;
if (typeof content === "string") {
inputTokens += estimateStringTokens(content);
} else if (Array.isArray(content)) {
for (const part of content) {
if (part && typeof part === "object") {
const text = (part as Record<string, unknown>).text;
if (typeof text === "string") inputTokens += estimateStringTokens(text);
}
}
}
}
}
const input = body.input;
if (Array.isArray(input)) {
for (const item of input) {
if (!item || typeof item !== "object") continue;
const text = (item as Record<string, unknown>).text;
if (typeof text === "string") inputTokens += estimateStringTokens(text);
}
}
if (typeof body.system === "string") {
inputTokens += estimateStringTokens(body.system);
}
// Reserved output budget: max_tokens / max_completion_tokens win; fall back
// to the default allowance.
const rawMax =
typeof body.max_tokens === "number"
? body.max_tokens
: typeof body.max_completion_tokens === "number"
? body.max_completion_tokens
: undefined;
const outputTokens =
typeof rawMax === "number" && rawMax > 0 ? Math.ceil(rawMax) : DEFAULT_OUTPUT_ALLOWANCE;
const totalTokens = Math.ceil(inputTokens * OVER_PROVISION) + outputTokens;
return { inputTokens, outputTokens, totalTokens };
}