-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Implement OpenAI token counting using tiktoken
#3447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d7f0b87
80a61f1
cc8cbf0
c1be8c1
1332cd8
cb5da87
6396f5d
46cd331
86a0b89
bacf788
acf86b0
6d2d4dd
9943173
75f29fa
6deaea2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,11 @@ | |
| from .._output import DEFAULT_OUTPUT_TOOL_NAME, OutputObjectDefinition | ||
| from .._run_context import RunContext | ||
| from .._thinking_part import split_content_into_text_and_thinking | ||
| from .._utils import guard_tool_call_id as _guard_tool_call_id, now_utc as _now_utc, number_to_datetime | ||
| from .._utils import ( | ||
| guard_tool_call_id as _guard_tool_call_id, | ||
| now_utc as _now_utc, | ||
| number_to_datetime, | ||
| ) | ||
| from ..builtin_tools import CodeExecutionTool, ImageGenerationTool, MCPServerTool, WebSearchTool | ||
| from ..exceptions import UserError | ||
| from ..messages import ( | ||
|
|
@@ -55,6 +59,7 @@ | |
| from . import Model, ModelRequestParameters, StreamedResponse, check_allow_model_requests, download_item, get_user_agent | ||
|
|
||
| try: | ||
| import tiktoken | ||
| from openai import NOT_GIVEN, APIConnectionError, APIStatusError, AsyncOpenAI, AsyncStream | ||
| from openai.types import AllModels, chat, responses | ||
| from openai.types.chat import ( | ||
|
|
@@ -1008,6 +1013,24 @@ def _inline_text_file_part(text: str, *, media_type: str, identifier: str) -> Ch | |
| ) | ||
| return ChatCompletionContentPartTextParam(text=text, type='text') | ||
|
|
||
| async def count_tokens( | ||
| self, | ||
| messages: list[ModelMessage], | ||
| model_settings: ModelSettings | None, | ||
| model_request_parameters: ModelRequestParameters, | ||
| ) -> usage.RequestUsage: | ||
| """Count the number of tokens in the given messages.""" | ||
| if self.system != 'openai': | ||
| raise NotImplementedError('Token counting is only supported for OpenAI system.') | ||
|
|
||
| model_settings, model_request_parameters = self.prepare_request(model_settings, model_request_parameters) | ||
| openai_messages = await self._map_messages(messages, model_request_parameters) | ||
| token_count = _num_tokens_from_messages(openai_messages, self.model_name) | ||
|
|
||
| return usage.RequestUsage( | ||
| input_tokens=token_count, | ||
| ) | ||
|
|
||
|
|
||
| @deprecated( | ||
| '`OpenAIModel` was renamed to `OpenAIChatModel` to clearly distinguish it from `OpenAIResponsesModel` which ' | ||
|
|
@@ -1804,6 +1827,26 @@ async def _map_user_prompt(part: UserPromptPart) -> responses.EasyInputMessagePa | |
| assert_never(item) | ||
| return responses.EasyInputMessageParam(role='user', content=content) | ||
|
|
||
| async def count_tokens( | ||
DouweM marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self, | ||
| messages: list[ModelMessage], | ||
| model_settings: ModelSettings | None, | ||
| model_request_parameters: ModelRequestParameters, | ||
| ) -> usage.RequestUsage: | ||
| """Count the number of tokens in the given messages.""" | ||
| if self.system != 'openai': | ||
| raise NotImplementedError('Token counting is only supported for OpenAI system.') | ||
|
|
||
| model_settings, model_request_parameters = self.prepare_request(model_settings, model_request_parameters) | ||
| _, openai_messages = await self._map_messages( | ||
| messages, cast(OpenAIResponsesModelSettings, model_settings or {}), model_request_parameters | ||
| ) | ||
| token_count = _num_tokens_from_messages(openai_messages, self.model_name) | ||
|
|
||
| return usage.RequestUsage( | ||
| input_tokens=token_count, | ||
| ) | ||
|
|
||
|
|
||
| @dataclass | ||
| class OpenAIStreamedResponse(StreamedResponse): | ||
|
|
@@ -2519,3 +2562,31 @@ def _map_mcp_call( | |
| provider_name=provider_name, | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| def _num_tokens_from_messages( | ||
| messages: list[chat.ChatCompletionMessageParam] | list[responses.ResponseInputItemParam], | ||
| model: OpenAIModelName, | ||
| ) -> int: | ||
| """Return the number of tokens used by a list of messages.""" | ||
| try: | ||
| encoding = tiktoken.encoding_for_model(model) | ||
| except KeyError: | ||
| encoding = tiktoken.get_encoding('o200k_base') | ||
|
|
||
| if 'gpt-5' in model: | ||
| tokens_per_message = 3 | ||
| final_primer = 2 # "reverse engineered" based on test cases | ||
| else: | ||
| # Adapted from https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken#6-counting-tokens-for-chat-completions-api-calls | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at the cookbook again, I think we should also try to implement support for counting the tokens of tool definitions: |
||
| tokens_per_message = 3 | ||
| final_primer = 3 # every reply is primed with <|start|>assistant<|message|> | ||
|
|
||
| num_tokens = 0 | ||
| for message in messages: | ||
| num_tokens += tokens_per_message | ||
| for value in message.values(): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit weird, as it assumes every string value in the class Message(TypedDict, total=False):
content: Required[ResponseInputMessageContentListParam]
"""
A list of one or many input items to the model, containing different content
types.
"""
role: Required[Literal["user", "system", "developer"]]
"""The role of the message input. One of `user`, `system`, or `developer`."""
status: Literal["in_progress", "completed", "incomplete"]
"""The status of item.
One of `in_progress`, `completed`, or `incomplete`. Populated when items are
returned via API.
"""
type: Literal["message"]
"""The type of the message input. Always set to `message`."""I don't think those |
||
| if isinstance(value, str): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also don't currently handle lists of strings properly, for example class ChatCompletionUserMessageParam(TypedDict, total=False):
content: Required[Union[str, Iterable[ChatCompletionContentPartParam]]]
"""The contents of the user message."""
role: Required[Literal["user"]]
"""The role of the messages author, in this case `user`."""
name: str
"""An optional name for the participant.
Provides the model information to differentiate between participants of the same
role.
"""
class ChatCompletionContentPartTextParam(TypedDict, total=False):
text: Required[str]
"""The text content."""
type: Required[Literal["text"]]
"""The type of the content part."""We shouldn't exclude that text from the count. Same for Unfortunately OpenAI makes it very hard for us to calculate this stuff correctly, but I'd rather have no
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I am up try to count the tokens for more complicated histories. Seems there are quite a few possible inputs based on your comment. Are there any test cases which I can use as a starting point which represent a more complicated structure?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @wirthual Not specifically, but if you look at the types in the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @wirthual Just found https://github.com/pamelafox/openai-messages-token-helper which may be worth using or looking at for inspiration |
||
| num_tokens += len(encoding.encode(value)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this (or the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The methods which download the encoding file are |
||
| num_tokens += final_primer | ||
| return num_tokens | ||
Uh oh!
There was an error while loading. Please reload this page.