feat: test token counting (#7943)

This commit is contained in:
Sarah Wooders
2025-12-22 14:12:52 -07:00
committed by Caren Thomas
parent 0a372b2540
commit f512d13bc9
2 changed files with 1864 additions and 6 deletions

View File

@@ -688,6 +688,10 @@ class AnthropicClient(LLMClientBase):
if thinking_enabled: if thinking_enabled:
betas.append("context-management-2025-06-27") betas.append("context-management-2025-06-27")
# Structured outputs beta - only for supported models
if model and _supports_structured_outputs(model):
betas.append("structured-outputs-2025-11-13")
if betas: if betas:
result = await client.beta.messages.count_tokens(**count_params, betas=betas) result = await client.beta.messages.count_tokens(**count_params, betas=betas)
else: else:
@@ -1074,13 +1078,21 @@ def convert_tools_to_anthropic_format(tools: List[OpenAITool], use_strict: bool
input_schema = tool.function.parameters or {"type": "object", "properties": {}, "required": []} input_schema = tool.function.parameters or {"type": "object", "properties": {}, "required": []}
# Use the older lightweight cleanup: remove defaults and simplify union-with-null. # Use the older lightweight cleanup: remove defaults and simplify union-with-null.
cleaned_schema = _clean_property_schema(input_schema) if isinstance(input_schema, dict) else input_schema # When using structured outputs (use_strict=True), also add additionalProperties: false to all object types.
cleaned_schema = (
_clean_property_schema(input_schema, add_additional_properties_false=use_strict)
if isinstance(input_schema, dict)
else input_schema
)
# Normalize to a safe "object" schema shape to avoid downstream assumptions failing. # Normalize to a safe "object" schema shape to avoid downstream assumptions failing.
if isinstance(cleaned_schema, dict): if isinstance(cleaned_schema, dict):
if cleaned_schema.get("type") != "object": if cleaned_schema.get("type") != "object":
cleaned_schema["type"] = "object" cleaned_schema["type"] = "object"
if not isinstance(cleaned_schema.get("properties"), dict): if not isinstance(cleaned_schema.get("properties"), dict):
cleaned_schema["properties"] = {} cleaned_schema["properties"] = {}
# Ensure additionalProperties: false for structured outputs on the top-level schema
if use_strict and "additionalProperties" not in cleaned_schema:
cleaned_schema["additionalProperties"] = False
formatted_tool: dict = { formatted_tool: dict = {
"name": tool.function.name, "name": tool.function.name,
"description": tool.function.description if tool.function.description else "", "description": tool.function.description if tool.function.description else "",
@@ -1099,13 +1111,14 @@ def convert_tools_to_anthropic_format(tools: List[OpenAITool], use_strict: bool
return formatted_tools return formatted_tools
def _clean_property_schema(schema: dict) -> dict: def _clean_property_schema(schema: dict, add_additional_properties_false: bool = False) -> dict:
"""Older schema cleanup used for Anthropic tools. """Older schema cleanup used for Anthropic tools.
Removes / simplifies fields that commonly cause Anthropic tool schema issues: Removes / simplifies fields that commonly cause Anthropic tool schema issues:
- Remove `default` values - Remove `default` values
- Simplify nullable unions like {"type": ["null", "string"]} -> {"type": "string"} - Simplify nullable unions like {"type": ["null", "string"]} -> {"type": "string"}
- Recurse through nested schemas (properties/items/anyOf/oneOf/allOf/etc.) - Recurse through nested schemas (properties/items/anyOf/oneOf/allOf/etc.)
- Optionally add additionalProperties: false to object types (required for structured outputs)
""" """
if not isinstance(schema, dict): if not isinstance(schema, dict):
return schema return schema
@@ -1133,16 +1146,20 @@ def _clean_property_schema(schema: dict) -> dict:
continue continue
if key == "properties" and isinstance(value, dict): if key == "properties" and isinstance(value, dict):
cleaned["properties"] = {k: _clean_property_schema(v) for k, v in value.items()} cleaned["properties"] = {k: _clean_property_schema(v, add_additional_properties_false) for k, v in value.items()}
elif key == "items" and isinstance(value, dict): elif key == "items" and isinstance(value, dict):
cleaned["items"] = _clean_property_schema(value) cleaned["items"] = _clean_property_schema(value, add_additional_properties_false)
elif key in ("anyOf", "oneOf", "allOf") and isinstance(value, list): elif key in ("anyOf", "oneOf", "allOf") and isinstance(value, list):
cleaned[key] = [_clean_property_schema(v) if isinstance(v, dict) else v for v in value] cleaned[key] = [_clean_property_schema(v, add_additional_properties_false) if isinstance(v, dict) else v for v in value]
elif key in ("additionalProperties",) and isinstance(value, dict): elif key in ("additionalProperties",) and isinstance(value, dict):
cleaned[key] = _clean_property_schema(value) cleaned[key] = _clean_property_schema(value, add_additional_properties_false)
else: else:
cleaned[key] = value cleaned[key] = value
# For structured outputs, Anthropic requires additionalProperties: false on all object types
if add_additional_properties_false and cleaned.get("type") == "object" and "additionalProperties" not in cleaned:
cleaned["additionalProperties"] = False
return cleaned return cleaned

File diff suppressed because one or more lines are too long