fix(count_tokens): price an inline file block instead of raising on it

`ChatCompletionFileObject` is in the union `_count_content_list` accepts, but
`file` was missing from its match, so every local count of a Responses
`input_file` raised `Invalid content item type: file`. On
/v1/responses/input_tokens that surfaced as an opaque 500 whenever the model's
provider counting API refused the block and the local tokenizer took over.

Count it the way the module already counts the same thing in Anthropic's
dialect: the filename like a document title, the inline bytes through the
image pricer.
This commit is contained in:
mateo-berri 2026-08-31 13:53:22 -07:00
parent c9908ffabb
commit b7da471784
2 changed files with 62 additions and 1 deletions

View file

@ -693,6 +693,26 @@ def _count_document_tokens(
)
def _count_file_tokens(
file_value: object,
count_function: TokenCounterFunction,
use_default_image_token_count: bool,
) -> int:
"""An OpenAI `file` block is the chat-completions spelling of a document, so it prices like one."""
if not isinstance(file_value, Mapping):
return 0
filename: Final = file_value.get("filename")
file_data: Final = file_value.get("file_data")
name_tokens: Final = count_function(filename) if isinstance(filename, str) and filename else 0
if not isinstance(file_data, str) or not file_data:
return name_tokens
return name_tokens + calculate_img_tokens(
data=file_data,
mode="auto",
use_default_image_token_count=use_default_image_token_count,
)
def _count_anthropic_content(
content: Mapping[str, Any],
count_function: TokenCounterFunction,
@ -778,6 +798,12 @@ def _count_content_list(
use_default_image_token_count,
default_token_count,
)
elif c["type"] == "file":
num_tokens += _count_file_tokens(
c.get("file"),
count_function,
use_default_image_token_count,
)
elif c["type"] in ("tool_use", "tool_result"):
num_tokens += _count_anthropic_content(
c,
@ -807,7 +833,7 @@ def _count_content_list(
raise ValueError(
f"Invalid content item type: {content_type}. "
f"Expected str or dict with 'type' field "
f"(text, image_url, image, document, tool_use, tool_result, thinking, tool_reference)."
f"(text, image_url, image, document, file, tool_use, tool_result, thinking, tool_reference)."
)
return num_tokens
except Exception as e:

View file

@ -1377,3 +1377,38 @@ def test_anthropic_document_title_and_context_add_their_tokens():
{"type": "document", "source": source},
]
)
def test_openai_file_block_prices_like_the_equivalent_anthropic_document():
"""An inline `file` is a `document` in the chat-completions dialect, so it must price identically, not raise.
Before the fix `file` was missing from the content-block match even though `ChatCompletionFileObject`
is in the union this counter accepts, so every local count of a Responses `input_file` raised
`Invalid content item type: file` and surfaced as a 500 on /v1/responses/input_tokens.
"""
prompt = {"type": "text", "text": "Summarize this file."}
inline_file = {
"type": "file",
"file": {"filename": "report.pdf", "file_data": "data:application/pdf;base64,JVBERi0xLjQK"},
}
document = {
"type": "document",
"title": "report.pdf",
"source": {"type": "base64", "media_type": "application/pdf", "data": "JVBERi0xLjQK"},
}
assert _count_user_content([prompt, inline_file]) == _count_user_content([prompt, document])
assert _count_user_content([prompt, inline_file]) > _count_user_content([prompt])
def test_openai_file_block_without_inline_bytes_counts_what_it_carries():
"""A `file` block naming an uploaded file has no bytes to price, so it adds only the filename's tokens."""
prompt = {"type": "text", "text": "Summarize this file."}
by_id = {"type": "file", "file": {"file_id": "file-abc123"}}
assert _count_user_content([prompt, by_id]) == _count_user_content([prompt])
named = {"type": "file", "file": {"file_id": "file-abc123", "filename": "report.pdf"}}
assert _count_user_content([prompt, named]) == _count_user_content(
[prompt, {"type": "text", "text": "report.pdf"}]
)