Phase 3.5 extraction & assembly

Model-aware token budgets from a generate-and-diff export of the Go ModelCapability catalog, with labeled character/rune estimates (ADR-0067).

Milestone 3.5.C.1 — Token Budget Calculator (Model-Aware, Estimate-Based)

Status: Completed Goal: Track C — Context Assembly Engine
Phase: 3.5 — Extraction & Context Assembly
Estimated effort: 2 days
Track: Track C — Context Assembly Engine
Depends on: Phase 2.5 ModelCapability registry (ADR-0041) + tokenizer family keys (ADR-0043)
ADR: ADR-0067
Follow-up: #690 exact HF/Qwen tokenization


Why This Milestone Exists

Every downstream stage in context assembly depends on knowing the usable token budget for the specific model being called. Phase 2.5 already curates that metadata in Go (BuiltInCapabilityCatalog). This milestone wires Python budget math to that catalog without inventing a live tokenizer/capability RPC (reserved for 3.5.C.6).


Non-Goals

  • Retrieval orchestration itself (3.5.C.2)
  • gRPC ContextAssemblyService / degradation contract (3.5.C.6)
  • Exact tiktoken or Hugging Face token counting (#690)
  • Cross-model comparison analytics

File layout (as shipped)

services/context/
  app/data/model_capabilities.v1.json   # generate-and-diff from Go catalog (package data)
  app/budget.py                         # BudgetCalculator / TokenBudget
  app/capability_catalog.py             # load/validate JSON
  app/estimate.py                       # labeled chars_div_4 / runes_div_3_5
packages/provider/scripts/export_capabilities/  # Go exporter + -check

Regenerate:

bash
go run ./packages/provider/scripts/export_capabilities \
  -o services/context/app/data/model_capabilities.v1.json

Design

Python
@dataclass(frozen=True)
class TokenBudget:
    context_window: int
    response_reserve: int
    safety_buffer: int
    usable_budget: int
    directive_tokens: int
    messages_tokens: int
    is_constrained: bool
    estimate_kind: str  # labeled: chars_div_4 | runes_div_3_5

class BudgetCalculator:
    def __init__(self, catalog: CapabilityCatalog | None = None): ...

    def calculate(self, model: str, messages: list[Message], directive: str) -> TokenBudget:
        cap = self._catalog.for_model(model)  # generated JSON; unknown model raises
        # estimate_tokens uses family policy from the same JSON (not a live TokenizerClient)
        ...

There is no TokenizerClient or network CapabilityRegistry in C.1. The catalog is the committed generate-and-diff artifact of packages/provider.BuiltInCapabilityCatalog.


Design note

Safety buffers are tokenizer-family-specific fractions from the export (o200k_base/cl100k_base 2%, claude 5%, unknown 10%), not a flat 10% guess. Token counts are labeled approximations (estimate_kind on every TokenBudget) until #690 lands exact backends.

Failure mode: unknown model IDs raise UnknownModelError (fail closed). Estimates always run locally — there is no “tokenizer unreachable” network path in C.1.


Success signals

  • TokenBudget computed from generate-and-diff catalog rows (not a hand-copied Python table)
  • Unknown models fail closed; estimates always labeled via estimate_kind
  • Unit tests: gpt-4o vs claude-sonnet-4-5 produce different safety_buffer values (catalog has no Llama row yet)
  • is_constrained when usable budget is below MIN_VIABLE_MEMORY_BUDGET (256)
  • CI capability-catalog-freshness fails on catalog/JSON drift
Edit on GitHub

Last updated on