Skip to content

feat(context): Add TokenBudget and pluggable tokenizers - #6824

Open
patelchaitany wants to merge 1 commit into
feast-dev:masterfrom
patelchaitany:context/01-token-budget
Open

feat(context): Add TokenBudget and pluggable tokenizers#6824
patelchaitany wants to merge 1 commit into
feast-dev:masterfrom
patelchaitany:context/01-token-budget

Conversation

@patelchaitany

Copy link
Copy Markdown
Contributor

What this PR does / why we need it:

Adds feast.context, a new module holding the token accounting that prompt assembly in an OnDemandFeatureView will build on. This is the first of a planned series; nothing in Feast imports it yet, so the change is additive and inert.

Tokenizers are pluggable the same way online stores and vector stores already are. TOKENIZER_CLASS_FOR_TYPE maps a built-in name to a class path, exactly as ONLINE_STORE_CLASS_FOR_TYPE does in repo_config.py, and anything that is not a built-in name is itself taken to be the fully-qualified path of a Tokenizer subclass, loaded through feast.importer.import_class. There is no registry and no mutable global state — adding a tokenizer means writing the class and passing its path:

TokenBudget.of(4096, "cl100k_base")
TokenBudget.of(4096, "my_pkg.tokenizers.LlamaTokenizer")

Feast ships cl100k_base and o200k_base (tiktoken) plus a dependency-free character estimate.

TokenBudget is a frozen dataclass. consume() returns a new budget instead of mutating, so a budget is safe to share across threads and to reuse between assemblies; try_consume() returns None rather than raising, which is the primitive that greedy section selection will use later. Tokenizers compare by value, so budgets built from the same tokenizer name are interchangeable as dict keys and set members.

budget = TokenBudget.of(4096, "cl100k_base")
budget.count_tokens("Hello world")    # 2
budget = budget.consume("Hello world")
budget.remaining                      # 4094

Degradation is explicit rather than silent. When tiktoken cannot be loaded, get_tokenizer() falls back to the character estimate and logs a warning; TokenBudget.of(..., fallback=False) makes that fatal for paths needing exact counts, and is_approximate lets a caller check without matching on the tokenizer name. TiktokenTokenizer counts with disallowed_special=(), so a feature value containing a literal <|endoftext|> is treated as ordinary text instead of raising.

Note on the dependency: tiktoken is not declared here, so the tests asserting exact encodings skip when it is absent. Happy to add a feast[llm] extra in this PR or a follow-up, whichever reviewers prefer.

Which issue(s) this PR fixes:

No GitHub issue. Tracked internally as RHOAIENG-80116.

Checks

  • I've made sure the tests are passing.
  • My commits are signed off (git commit -s)
  • My PR title follows conventional commits format

Testing Strategy

  • Unit tests
  • Integration tests
  • Manual tests
  • Testing is not required for this change

sdk/python/tests/unit/context/ — 61 tests with tiktoken installed, 51 passing and 10 skipped without it. Covers name and class-path resolution, rejection of a class that is merely named Tokenizer, the fallback in both directions, budget arithmetic and immutability, and exact counts for both shipped encodings.

Misc

New module, no existing behaviour touched. ruff check, ruff format --check and mypy are clean across the eight files.

Create the feast.context module with the token accounting that prompt
assembly in an OnDemandFeatureView builds on.

Tokenizers resolve the way online store types do in repo_config: a
built-in name maps to a class path in TOKENIZER_CLASS_FOR_TYPE, and
anything else is itself the path of a Tokenizer subclass with a
no-argument constructor, loaded through import_class. Adding a tokenizer
is therefore what adding a vector store is - write the class, pass its
path, no registration call and no mutable global state. Feast ships
cl100k_base, o200k_base and a dependency-free character estimate.

TokenBudget is a frozen dataclass holding a resolved Tokenizer:
consume() returns a new budget rather than mutating, and try_consume()
returns None instead of raising, which is the primitive
priority_select() will use to fill a budget greedily. TokenBudget.of()
resolves a name, class path or instance and can refuse the fallback;
is_approximate reports whether counts are exact. Tokenizers compare by
value, so budgets built from the same name are interchangeable as dict
keys and set members.

When tiktoken cannot be loaded, get_tokenizer() degrades to the
character estimate with a logged warning; fallback=False makes it fatal
for paths that need exact counts. TiktokenTokenizer counts with
disallowed_special=(), so a feature value containing a literal
"<|endoftext|>" is treated as ordinary text instead of raising.

tiktoken is not declared as a dependency yet, so the tests that assert
exact encodings skip when it is absent.

Part of RHOAIENG-80116 (PR 1/9).

Signed-off-by: Chaitany Patel <patelchaitany93@gmail.com>
@patelchaitany
patelchaitany requested a review from a team as a code owner September 10, 2026 08:05
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 47.08%. Comparing base (81e1546) to head (dd7b805).
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##           master    #6824   +/-   ##
=======================================
  Coverage   47.08%   47.08%           
=======================================
  Files         419      419           
  Lines       51877    51877           
  Branches     7525     7525           
=======================================
  Hits        24428    24428           
  Misses      25700    25700           
  Partials     1749     1749           
Flag Coverage Δ *Carryforward flag
go-feature-server 30.58% <ø> (ø)
python-unit 48.39% <ø> (ø) Carriedforward from 81e1546

*This pull request uses carry forward flags. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 81e1546...dd7b805. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants