Skip to content

feat: report input and output token estimates on tool-call events - #54

Draft
naji247 wants to merge 6 commits into
mainfrom
feat/token-estimates
Draft

naji247 wants to merge 6 commits into
mainfrom
feat/token-estimates

Conversation

@naji247

@naji247 naji247 commented Sep 19, 2026

Copy link
Copy Markdown
Member

Summary

Tool-call events now carry input_tokens and output_tokens: estimates of how many tokens the calling model spent on the tool arguments and how many it reads back from the tool result. AgentCat uses them for the oversized-tool-response rule and for per-tool token trends, and until now the server estimated both from the stored, redacted event payload. The SDK is the one place where the raw strings exist, so the estimate now happens there.

This is a patch release: agentcat 2.1.1 on PyPI.

What changes for you

  • Every mcp:tools/call event includes input_tokens and output_tokens. No configuration is needed and no option changes.
  • The numbers describe the payload as the model saw it, not the analytics envelope: the input side counts the raw tool arguments (including AgentCat's injected context, session_id, and agent_id), and the output side counts only the text in the result's content blocks. The tool name, transport headers, structuredContent, isError, and the session mint-back text do not count.
  • Both values are computed before your redact_sensitive_information and redact_event hooks run and before sanitization and truncation, so they describe the original size even when the stored strings are redacted or truncated. If that size must not leave your server, drop the two fields in redact_event.
  • A call that arrives with no arguments counts as an empty object (one token), because the adapters hand the funnel an empty mapping either way.
  • A failure to estimate never affects your MCP server: the estimator never throws, and the tool result reaches the client unchanged in every case.
  • Expect token counts about 10 percent lower than the previous server-side estimates for the same responses, because the JSON envelope and escaping no longer count. Projects with a tuned oversized-tool-response limit will see fewer instances.

How the estimate works

ceil(utf8_bytes / 3.5), clamped to the server's integer column. The divisor comes from a measurement of 1,240 production tool responses: the text a model reads runs 3.71 bytes per token on OpenAI-family tokenizers and roughly 3.15 on Anthropic-family tokenizers, so 3.5 splits the difference. The same rules and the same test vectors are pinned across the TypeScript, Python, and Go SDKs, so all three report identical numbers for identical payloads.

Dependency

Requires agentcat-api 1.0.2, which adds the two fields to the publish request. This PR pins the version once the package is published; until then it stays a draft.

Testing

  • tests/test_token_estimate.py: the shared vector table (ASCII, accented and CJK text, HTML characters, empty and image-only content, resource blocks, the fallback for results without a content list, the int32 clamp, and the never-throw guards).
  • tests/test_token_estimates_integration.py: real tool calls through every supported server flavor and MCP generation, asserting the counts on captured events, the error-result path, that redaction and truncation leave the counts unchanged, and that a broken estimator never breaks the tool call.
  • Full suite passes on both the modern and the legacy mcp generation. A cross-SDK brief, 2026-09-19-cross-sdk-token-estimates.md, documents the shared rules and vectors for maintainers of the other SDKs.

… review

Drop json.dumps's default=str so an unserializable argument is omitted
rather than counted (TypeScript and Go already omit it), and encode with
surrogatepass so a lone surrogate off the wire counts its 3 bytes instead of
dropping the whole field. Add coverage for both, add a truncation leg to the
tools/call ordering test, and note the two Go-only edge cases (empty-object
counting, isError responses) plus serialization corner cases in the
cross-SDK brief.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant