oc_chat
Loading...
Searching...
No Matches
tokenizer.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/core/tokenizer.py
4
5"""
6Tokenizer-based token counting.
7
8Purpose
9-------
10Provides accurate token counting using tiktoken when available,
11with graceful fallback to character-based heuristics.
12
13Responsibilities
14----------------
15- Count tokens in message lists and strings
16- Lazy-load tiktoken encoding (cached per process)
17- Provide consistent token counting across the codebase
18
19Diagnostics
20-----------
21Domain: OC_CHAT.CTX
22Levels:
23 L2 — tokenizer fallback notice (tiktoken missing)
24 L3 — tokenizer initialization, encoding selection
25 L4 — per-call token counts
26
27Contracts
28---------
29- Returns integer token count
30- Works without tiktoken installed (falls back to heuristic)
31- Uses cl100k_base encoding (GPT-4/DeepSeek compatible)
32
33Dependencies
34------------
35- tiktoken (lazy; the cl100k_base encoding, optional — graceful fallback if not installed)
36- oc_diagnostics (structured debug logging via _dbg)
37"""
38from __future__ import annotations
39
40from typing import TYPE_CHECKING, Any
41
42from oc_diagnostics import _dbg
43
44if TYPE_CHECKING:
45 pass
46
47# Lazy-loaded tokenizer encoding
48_encoding = None
49
50
52 """Lazy-load tiktoken encoding. Returns None if tiktoken not available.
53
54 Uses cl100k_base encoding which is compatible with GPT-4 and DeepSeek
55 tokenization schemes.
56 """
57 func = "_get_encoding"
58 global _encoding
59 if _encoding is not None:
60 return _encoding
61
62 try:
63 import tiktoken
64 _encoding = tiktoken.get_encoding("cl100k_base")
65 _dbg("OC_CHAT.CTX", func, "tiktoken cl100k_base loaded", 3)
66 return _encoding
67 except ImportError:
68 _dbg("OC_CHAT.CTX", func,
69 "tiktoken not available, using fallback heuristic", 2)
70 return None
71
72
73def count_tokens(text: str) -> int:
74 """Count tokens in a string using tiktoken or fallback heuristic.
75
76 Uses cl100k_base encoding (GPT-4/DeepSeek compatible).
77 Falls back to ~4 characters per token if tiktoken not installed.
78
79 Args:
80 text: The string to count tokens for.
81
82 Returns:
83 Estimated token count.
84 """
85 if not text:
86 return 0
87
88 enc = _get_encoding()
89 if enc is not None:
90 return len(enc.encode(text))
91
92 # Fallback heuristic: ~4 characters per token
93 return len(text) // 4
94
95
96def count_message_tokens(messages: list[dict[str, Any]]) -> int:
97 """Count tokens in a message list.
98
99 This is the replacement for the old _estimate_tokens() heuristic.
100 Counts content, reasoning_content, and tool_calls for each message.
101
102 Args:
103 messages: List of message dicts with 'role', 'content', etc.
104
105 Returns:
106 Total estimated token count for the message list.
107 """
108 if not messages:
109 return 0
110
111 total = 0
112
113 for msg in messages:
114 # Message structure overhead (~4 tokens per message for formatting)
115 total += 4
116
117 # Content
118 content = msg.get("content", "")
119 if isinstance(content, str):
120 total += count_tokens(content)
121
122 # Role token
123 role = msg.get("role", "")
124 if role:
125 total += 1
126
127 # Reasoning content (DeepSeek thinking mode)
128 reasoning = msg.get("reasoning_content", "")
129 if isinstance(reasoning, str):
130 total += count_tokens(reasoning)
131
132 # Tool calls (if present in assistant messages)
133 tool_calls = msg.get("tool_calls", [])
134 if tool_calls and isinstance(tool_calls, list):
135 for tc in tool_calls:
136 fn = tc.get("function", {})
137 if isinstance(fn, dict):
138 total += count_tokens(fn.get("name", ""))
139 total += count_tokens(fn.get("arguments", ""))
140 # Tool call ID and type overhead
141 total += 4
142
143 return total
int count_message_tokens(list[dict[str, Any]] messages)
Definition tokenizer.py:96
int count_tokens(str text)
Definition tokenizer.py:73