feat(cli): add litellm-token-count subcommand for offline token counting (Fixes #34772)#34775
Draft
Harsh23Kashyap wants to merge 5 commits into
Draft
Conversation
Counts the tokens a model would see for a given text, file, or
message list, using the public litellm.token_counter helper. Reuses
the same tokenizer selection that litellm.completion uses, so the
count matches what a real call would produce. No network calls are
made and no provider credentials are required.
Three input modes (mutually exclusive):
--text "..." raw text string
--file path/to/file.txt read from a UTF-8 file (- reads stdin)
--messages <json-list> parse an OpenAI-style message list
(- reads JSON from stdin)
Output is a single line by default (model=... mode=... count=N) and
--json emits a {model, mode, count} object for scripts. Privacy: the
input text and messages JSON are never echoed to stdout, only the
count.
The programmatic helper count_tokens(model, *, text, file, messages)
returns a TokenCount dataclass and raises ValueError on input
validation errors so callers can catch them separately from runtime
errors. Mirrors the cost-estimate and doctor subcommands in the same
litellm/cli/ package; registers a litellm-token-count entry point in
pyproject.toml.
Refs BerriAI#34772
23 tests across two layers: count_tokens (programmatic API) - happy paths for --text, --file, --messages - mutual exclusion of input options - missing input source - empty model - invalid messages JSON - non-list messages - missing file surfaces as ValueError - token_counter failure wraps as RuntimeError - to_jsonable round-trip cli (CLI surface) - basic text invocation prints count - --json produces valid JSON - --file reads from disk - --messages parses JSON - mutually exclusive flags exit 2 - missing input exits 2 - missing --model exits 2 - invalid JSON in --messages exits 2 - non-list --messages exits 2 - --file not found exits 2 - input text never echoed to stdout (privacy) - messages JSON never echoed to stdout (privacy)
…strict TID251 budget ruff-strict gate flagged 2 new TID251 violations for the cast() import + call. The TypedDict narrowing was added to satisfy basedpyright but the downstream token_counter call already takes list[AllMessageValues] (which absorbs list[Any]), so the TypedDict was not pulling its weight. Returning list[Any] from the messages resolver removes both TID251 violations, and converting the remaining Optional[X] to X | None removes 9 UP045 violations. Net effect: ruff-strict gate now reports no new violations; 23 tests still pass; format clean; basedpyright is +3 vs the prior cost_estimate (11 vs 8), all in the same reportAny noise category that already exists across the repo.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
LIT009 bans the use of "# type: ignore" comments. The previous patch on this branch added one when calling _resolve_messages_payload(messages) at the end of _resolve_input, because the type checker did not narrow messages to str after the mutual-exclusion checks. The replacement is a runtime assert that mirrors the already-performed mutual-exclusion logic: by the time we reach the fall-through, the only path that survives is the messages branch, so messages is not None. The assert also serves as a tripwire if a future refactor breaks the mutual-exclusion invariants. ruff-strict gate: OK. type-discipline gate: OK. 23 tests pass.
The CLI test directory was added in prior PRs (litellm-doctor and litellm-cost-estimate) but never registered in the misc unit-test workflow, so codecov was reporting 0% patch coverage for the CLI sources and the CLI tests were not actually being run by CI. This adds tests/test_litellm/cli to the test-path list in the misc job so the new token-count tests (and the existing doctor + cost-estimate tests) run on every PR and patch coverage is reported.
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Problem this solves:
litellm cost-estimateandlitellm doctoralready ship offline CLIs; token counting is the missing thirdlitellm.utils.token_counter()exists but requires a Python import to useHow it solves it:
litellm token-countsubcommand that wrapslitellm.token_counter()--text,--file, and--messages(each with stdin via-) as mutually-exclusive inputsmodel=... mode=... count=Nby default, or JSON via--jsonlitellm-cost-estimateandlitellm-doctorpattern inlitellm/cli/Relevant issues
litellm token-countCLI subcommand for offline token counting #34772Linear ticket
Pre-Submission checklist
@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Real CLI run against the local install (no proxy, no provider, no mock); commit
2fec98dfdd(HEAD oflitellm_feat_token_count):Real count values are non-zero (2 for "hello world", 8 for a single user message with role+content overhead), so a regression that silently returns 0 would fail the tests.
Type
Changes
Two commits on
litellm_feat_token_count(offlitellm_internal_staging=24123269cc):feat(cli): add litellm-token-count subcommand for offline token counting; addslitellm/cli/token_count.py(CLI +count_tokens()helper +TokenCountdataclass +TokenCountMessageTypedDict), the emptylitellm/cli/__init__.py, and thelitellm-token-countentry point inpyproject.toml.test(cli): cover litellm-token-count pass / fail / invalid-input paths; addstests/test_litellm/cli/test_token_count.pywith 23 tests across the programmatic helper and the CLI surface (pass paths, JSON output, mutually-exclusive flags, missing model, missing file, invalid JSON, non-list messages, privacy: text/messages never echoed to stdout).Files added:
litellm/cli/__init__.py,litellm/cli/token_count.py,tests/test_litellm/cli/test_token_count.py. Files modified:pyproject.toml(one line; new[project.scripts]entry mirroringlitellm-cost-estimateandlitellm-doctor).No existing public Python API changes;
litellm.utils.token_counter()keeps its current signature. The newcount_tokenshelper is a thin wrapper that calls the existing function with sensible defaults.QA runbook
Not a proxy change, so no live-proxy QA needed. The CLI is exercised end-to-end via the CliRunner tests in
tests/test_litellm/cli/test_token_count.py(23 tests, all passing locally). To verify locally:Final Attestation