IBEX Harness
DocsBenchmarksBlogChangelogRoadmap
GitHub
IBEX Harness

Documentation

Architecture Decision RecordsADR-0002: Repository foundation bootstrapADR-0003: Branch protection and merge policyADR-0004: Protobuf and code generation policyADR-0005: Postgres migration strategyADR-0006: Auth protobuf contract (`ibex.auth.v1`)ADR-0007: Auth token validation implementationADR-0008: Security scanning and CI quality gatesADR-0009: Permission bitmap layoutADR-0010: Cryptography policyADR-0011: Proxy auth gRPC client and middlewareADR-0012: Proxy request normalization (OpenAI chat)ADR-0013: Proxy input validation and stable error envelopeADR-0014: Core domain migration sequencingADR-0015: Proxy rate limit skeleton (Phase 1)ADR-0016: Proxy agent identity verification (Phase 1)ADR-0017: Request ID and trace context strategy (Phase 1)ADR-0018: Graceful shutdown contract (Phase 1)ADR-0019: OpenTelemetry provider configuration (Phase 1)ADR-0020: Shared package boundaries — `packages/config` and `packages/apierror`ADR-0021: Prometheus Metric Catalog (Phase 1)ADR-0022: Health check contract (Phase 1)ADR-0023: Docs site architecture (Phase 1.5)ADR-0024: Benchmark data publishing modelADR-0025: LLM provider abstractionADR-0026: OpenAI client designADR-0027: Streaming dual-write strategyADR-0028: Auth cache designADR-0029: Token revocation propagation via Redis pub/subADR-0030: Directive versioning strategyADR-0031: System prompt injection strategyADR-0032: Session data model and retentionADR-0033: ClickHouse llm_traces schema and retentionADR-0034: Proxy overhead performance measurement methodologyADR-0035: Chat Idempotency-Key Redis dedupeADR-0038: Context assembly service design and gRPC contractADR-0039: Proxy Postgres ownership for session and directive storesADR-0040: Anthropic provider adapterADR-0041: Model capability registryADR-0042: Self-hosted OpenAI-compatible LLM adapterADR-0043: Tokenizer registry architectureADR-0044: Non-streaming response pipelineADR-0045: Streaming response transformationADR-0046: Embedder interface and profile registryADR-0047: Memory temporal validity foundationADR-0048: Memory multi-label categoriesADR-0049: Memory relationship graph readinessADR-0050: MCP server skeleton (transport, auth, audit)ADR-0051: Local LGTM observability stack (Phase 2.5 exit pull-forward)ADR-0052: Memory schema v2 expand (HNSW, quality columns)ADR-0053: Vector store abstraction and composite scoring v2
ADRs›ADR-0043: Tokenizer registry architecture
ADRs

ADR-0043: Tokenizer registry architecture

Architecture decision record 0043 — per-family tokenizer registry, bundled BPE assets, claude estimate policy, and proxy bootstrap wiring.

ADR-0043: Tokenizer registry architecture

  • Status: Accepted
  • Date: 2026-08-22
  • Authors: IBEX Harness team
  • Milestone: 2.5.G2.M1 Tokenizer registry

Context

ADR-0041 defines TokenizerFamily join keys (o200k_base, cl100k_base, claude, unknown on overlays). Phase 3.5 token budget calculation needs family-accurate pre-flight counts — using OpenAI tiktoken for Anthropic or self-hosted models is systematically wrong.

SPIKE findings (G2.M1):

FamilyBackend (v1)Rationale
o200k_basePure Go tiktoken-go + bundled o200k_base.tiktoken in packages/tokenizer/assets/Exact counts; air-gap safe (no runtime download)
cl100k_basetiktoken-go + embedded BPE via tiktoken-go-loader offline loaderExact counts; no network
claudeDocumented estimate (ceil(rune_count / 3.5))Anthropic Claude 3+ has no public offline vocab; official path is count_tokens API — do not map to tiktoken
llama3 / qwen2Deferred (Phase 2.5+ or Python tokenizer-service)CGo (daulet/tokenizers) complicates cross-compile; prefer service path when first HF overlay ships
unknown (overlays)Not registeredFail at CountForModel; operators must set explicit family before accurate budgets

Proxy hot path (ADR-0034) must not block on counting in G2 — registry construction + advisory /ready self-test only.

Decision

1) packages/tokenizer contract

Go
type Tokenizer interface {
    Family() string
    Count(ctx context.Context, text string) (int, error)
}
  • Registry maps TokenizerFamily → Tokenizer (mirrors provider.Registry pattern).
  • NewLocalRegistry(assetDir) builds default v1 backends.
  • ValidateCatalogCoverage(catalog, reg) fails closed when any catalog row (non-unknown family) lacks an impl.
  • CountForModel(ctx, catalog, reg, model, text) resolves model → capability → family → Count.

2) Fail-closed startup, conservative runtime (Phase 3.5)

  • Registry construction validates catalog coverage at proxy bootstrap.
  • Missing family → startup error (ErrMissingTokenizer).
  • Overlay with unknown → allowed in catalog validation; CountForModel returns error (no silent tiktoken substitution).
  • Phase 3.5 may degrade to char estimate on remote failure — not in G2.

3) Air-gap asset policy

  • o200k_base.tiktoken is committed under packages/tokenizer/assets/ (embedded via go:embed).
  • cl100k_base loads from tiktoken-go-loader embedded assets.
  • Optional IBEX_TOKENIZER_ASSET_DIR overrides BPE files by basename (operator-managed updates).
  • No runtime Hugging Face downloads in v1.

4) Claude estimate (explicit, not exact)

claude backend implements Estimator with IsEstimate() == true. Formula:

tokens = 0                         if text is empty
tokens = ceil(rune_count / 3.5)    otherwise  (implemented as (runes*2+6)/7)

Consumers in Phase 3.5 may use Claude counts only after establishing a measured, versioned error bound against Anthropic count_tokens or an equivalent ground-truth source. Until then, apply family-specific safety buffers and do not treat the heuristic as directionally safe for budget enforcement.

5) Config surface (v1)

Variablev1 behavior
IBEX_TOKENIZER_MODElocal only; service / dual rejected at validate
IBEX_TOKENIZER_ASSET_DIROptional BPE override directory

Deferred: IBEX_TOKENIZER_SERVICE_URL, IBEX_TOKENIZER_TIMEOUT_MS.

6) Proxy wiring (advisory)

  • Bootstrap builds tokenizer registry after capability catalog merge.
  • /ready advisory checker tokenizer: runs RunSelfTest on default vectors.
  • Metrics: ibex_tokenizer_count_total{family,result}, ibex_tokenizer_count_duration_seconds{family} (observed when counting is invoked — not on Complete hot path in G2).

7) Input bounds and security

  • Max input: 100 KiB (MaxCountTextBytes, aligned with ADR-0013 message limit).
  • No raw text in logs — log family, model_id, text_len, duration, error class only.
  • Counting is stateless; no tenant data in algorithm.

Consequences

Positive:

  • Stable counting contract keyed by ADR-0041 families before Phase 3.5 budget math.
  • OpenAI families match offline ground-truth vectors in CI.
  • Air-gap deployments do not require OpenAI blob downloads at runtime.

Negative / follow-ups:

  • Claude counts are estimates until API or service path lands.
  • HF families (llama3, qwen2) require a follow-up milestone + allowlist extension in capability.go.
  • Bundled o200k_base.tiktoken (~3.6 MiB) increases repo size; version bumps need changelog + vector re-verify.

References

  • ADR-0041 Model capability registry
  • ADR-0034 Performance methodology
  • ENVIRONMENT_VARIABLES.md — IBEX_TOKENIZER_*

Was this page helpful?

Edit on GitHub

Last updated on

PreviousADR-0042: Self-hosted OpenAI-compatible LLM adapterNextADR-0044: Non-streaming response pipeline

On this page

  • Context
  • Decision
  • 1) packages/tokenizer contract
  • 2) Fail-closed startup, conservative runtime (Phase 3.5)
  • 3) Air-gap asset policy
  • 4) Claude estimate (explicit, not exact)
  • 5) Config surface (v1)
  • 6) Proxy wiring (advisory)
  • 7) Input bounds and security
  • Consequences
  • References
0%