fix(agent): tolerate schema-valued "type" keys in the watchdog payload estimator (#104793)
The image-part membership test in _payload_chars() ran on every dict in the request, so a tool JSON Schema with a parameter named "type" (its value is the sub-schema dict) or a multi-type value like ["string", "null"] raised TypeError: unhashable type before the request reached the provider. Guard the membership test with isinstance(str): structured "type" values are payload data, not content parts, and keep the legacy walk for them.
This commit is contained in:
@@ -339,7 +339,10 @@ def _payload_chars(value: Any, image_cost: int) -> int:
|
||||
if value is None:
|
||||
return 0
|
||||
if isinstance(value, dict):
|
||||
if value.get("type") in _IMAGE_PART_TYPES and any(k in value for k in ("image_url", "image", "source", "file_id")):
|
||||
part_type = value.get("type")
|
||||
# JSON-Schema nodes may hold a sub-schema (``properties.type``) or a multi-type list
|
||||
# under the "type" key; only scalar content-part types can ever match (#104793).
|
||||
if isinstance(part_type, str) and part_type in _IMAGE_PART_TYPES and any(k in value for k in ("image_url", "image", "source", "file_id")):
|
||||
return _image_part_chars(value, image_cost)
|
||||
return sum(len(str(k)) + 6 + _payload_chars(v, image_cost) for k, v in value.items())
|
||||
if isinstance(value, list):
|
||||
|
||||
@@ -31,3 +31,24 @@ def test_text_payloads_and_tool_schemas_keep_the_legacy_estimate():
|
||||
"tools": [{"type": "function", "function": {"name": "vision", "parameters": {"type": "image"}}}]}
|
||||
legacy = (sum(len(str(m)) for m in payload["messages"]) + len(str(payload["tools"]))) // 4
|
||||
assert abs(estimate_request_context_tokens(payload) - legacy) < legacy * 0.02
|
||||
|
||||
|
||||
def test_schema_nodes_with_structured_type_values_keep_the_legacy_estimate():
|
||||
"""A JSON-Schema ``properties`` dict whose ``type`` KEY holds a sub-schema dict, or a multi-type
|
||||
list like ``["string", "null"]``, is payload data — not an image part. The membership test must
|
||||
not raise ``TypeError: unhashable type`` before the request reaches the provider (#104793)."""
|
||||
def _tools(param_name):
|
||||
return [{"type": "function", "function": {"name": "memory_search", "parameters": {"type": "object", "properties": {
|
||||
param_name: {"type": "string", "enum": ["episode", "semantic"]},
|
||||
"maybe": {"type": ["string", "null"]},
|
||||
}}}}]
|
||||
messages = [{"role": "user", "content": "hi"}]
|
||||
payload = {"messages": messages, "tools": _tools("type")}
|
||||
renamed = {"messages": messages, "tools": _tools("kind")} # equal-length key: identical walk
|
||||
assert estimate_request_context_tokens(payload) == estimate_request_context_tokens(renamed)
|
||||
# A real image part in the same request is still priced at the learned cost.
|
||||
chat = {"messages": [{"role": "user", "content": [
|
||||
{"type": "text", "text": "look"},
|
||||
{"type": "image_url", "image_url": {"url": _B64}},
|
||||
]}], "tools": _tools("type")}
|
||||
assert DEFAULT_IMAGE_TOKEN_COST <= estimate_request_context_tokens(chat) < DEFAULT_IMAGE_TOKEN_COST + 400
|
||||
|
||||
Reference in New Issue
Block a user