perf(core): cache tool openai-function JSON-char count

count_tokens_approximately was calling json.dumps(tool_dict) and throwing
away everything but the length on every invocation — even though the dict
returned by convert_to_openai_tool(tool) is stable for a given tool. Stash
the char count on the tool instance under _openai_function_chars (paired
with the _openai_function_dict schema cache from the previous commit).
BaseTool.__setattr__ pops both keys on mutation of args_schema / description
/ name so dynamic tool re-registration or in-place edits invalidate
correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sydney Runkle
2026-04-24 09:44:22 -04:00
parent 06e351a497
commit 43faee1fa5
2 changed files with 16 additions and 3 deletions

View File

@@ -2247,12 +2247,24 @@ def count_tokens_approximately(
last_ai_total_tokens: int | None = None
approx_at_last_ai: float | None = None
# Count tokens for tools if provided
# Count tokens for tools if provided. For BaseTool instances we stash the
# JSON-serialized length on the tool under `_openai_function_chars` (paired
# with the `_openai_function_dict` schema cache) so successive calls don't
# re-run json.dumps over a dict that hasn't changed. `BaseTool.__setattr__`
# pops both keys when schema-affecting fields mutate, so dynamic tool
# re-registration or in-place edits invalidate this correctly.
if tools:
tools_chars = 0
for tool in tools:
tool_dict = tool if isinstance(tool, dict) else convert_to_openai_tool(tool)
tools_chars += len(json.dumps(tool_dict))
if isinstance(tool, dict):
tools_chars += len(json.dumps(tool))
continue
cached_chars = tool.__dict__.get("_openai_function_chars")
if cached_chars is None:
tool_dict = convert_to_openai_tool(tool)
cached_chars = len(json.dumps(tool_dict))
tool.__dict__["_openai_function_chars"] = cached_chars
tools_chars += cached_chars
token_count += math.ceil(tools_chars / chars_per_token)
for message in converted_messages:

View File

@@ -561,6 +561,7 @@ class ChildTool(BaseTool):
self.__dict__.pop("tool_call_schema", None)
self.__dict__.pop("args", None)
self.__dict__.pop("_openai_function_dict", None)
self.__dict__.pop("_openai_function_chars", None)
@property
def is_single_input(self) -> bool: