Reviewing PR #3's iter_image_url_values (which correctly collapsed four traversals into one, per the capability-gate-followups plan): a part typed image_url but carrying neither a nested image_url.url nor a bare url key now yields nothing at all instead of an empty string, so it vanished from every derived check instead of failing them. Confirmed live before this fix: has_images read False (dodging the vision gate) and _local_vision_data_uris_ok read True (an SSRF guard passing vacuously on zero yielded items) for exactly this shape -- both regressions from what the four original hand-rolled loops did. Now every image_url-typed part yields exactly one string, "" when nothing resolves, which fails startswith("data:") and still counts toward any()/sum() -- matching pre-PR behavior. Two regression tests added. Also renames bugs_to_fix/ to code_reviews/, since this is the second report that's gone in there and "bugs to fix" undersold what the first one turned into -- and adds a review report for PR #3 alongside the plan it was implementing.
104 lines
4.5 KiB
Python
104 lines
4.5 KiB
Python
"""Detect request-side capability requirements from an OpenAI-format body.
|
|
|
|
Like ``routing.py``, ``scoring.py`` and ``tiering.py``, this module is free of
|
|
I/O: the request body comes in as a dict and capabilities come out as a value
|
|
object. ``dispatcher.py`` owns the parsing of the HTTP request into that dict.
|
|
|
|
Detection is read from the request body, not inferred by a classifier. This
|
|
mirrors the existing ``tools_present`` / ``min_tool_proficiency`` philosophy in
|
|
``routing.py``: a local classifier cannot reliably identify agentic or visual
|
|
work — asked to label unambiguous tool-use prompts it got 1-2 of 6, and vision
|
|
is the same class of problem. But the request states both exactly, in the
|
|
``tools`` array and in an ``image_url`` part, and reading them is free. When
|
|
the capability matters, it is a hard filter (a model that cannot be trusted
|
|
with tools that exist is not a candidate), never a weighted preference.
|
|
|
|
``has_reasoning_request`` is the one flag that is informational only: it is
|
|
recorded so dispatcher can observe it, but never fed to a routing filter.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterator
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
# OpenAI response_format.types that require the model to support JSON mode.
|
|
JSON_MODE_TYPES = frozenset({"json_object", "json_schema"})
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RequestCapabilities:
|
|
has_images: bool = False
|
|
require_json_mode: bool = False
|
|
tools_present: bool = False
|
|
has_reasoning_request: bool = False
|
|
|
|
|
|
def detect_capabilities(body: dict[str, Any]) -> RequestCapabilities:
|
|
"""Read the capability requirements stated by an OpenAI-format request body.
|
|
|
|
``has_images`` scans ALL messages, not just the last: vision is a property
|
|
of the whole conversation, and an image in an earlier turn still has to be
|
|
understood by whoever answers the latest one. A message whose ``content`` is
|
|
a list contributes True if any part is a dict whose ``type`` is
|
|
``image_url``. String content and non-image parts never trigger it; a raw
|
|
base64 string sitting in a text part is not an image_url and must not be
|
|
matched.
|
|
|
|
``require_json_mode`` reads ``response_format.type`` against
|
|
``JSON_MODE_TYPES``. ``text`` and an absent ``response_format`` read False.
|
|
|
|
``tools_present`` is truthy exactly when the body carries a ``tools`` array.
|
|
|
|
``has_reasoning_request`` is True if the body carries either a
|
|
``reasoning_effort`` key or a ``reasoning`` key. This flag is recorded for
|
|
observation only and is never consumed by a routing filter.
|
|
"""
|
|
has_images = _any_message_has_images(body.get("messages") or [])
|
|
response_format = body.get("response_format")
|
|
require_json_mode = (
|
|
isinstance(response_format, dict)
|
|
and response_format.get("type") in JSON_MODE_TYPES
|
|
)
|
|
return RequestCapabilities(
|
|
has_images=has_images,
|
|
require_json_mode=require_json_mode,
|
|
tools_present=bool(body.get("tools")),
|
|
has_reasoning_request="reasoning_effort" in body or "reasoning" in body,
|
|
)
|
|
|
|
|
|
def iter_image_url_values(messages: list[Any]) -> Iterator[str]:
|
|
"""Every image_url part's URL/data-URI string, across all messages.
|
|
|
|
Yields exactly one string per image_url-typed part, even when no URL can
|
|
be resolved from it (empty string). A part that declares itself an image
|
|
but carries no readable value must still count toward "this conversation
|
|
has an image" and still fail the "every part is a safe data: URI" check
|
|
-- silently skipping it, as an earlier version of this function did,
|
|
dropped it out of both checks: a degenerate part could then dodge the
|
|
vision-capability gate AND slip past the SSRF guard that only holds when
|
|
every yielded value is verified to start with ``data:``.
|
|
"""
|
|
for message in messages:
|
|
if not isinstance(message, dict):
|
|
continue
|
|
content = message.get("content")
|
|
if not isinstance(content, list):
|
|
continue
|
|
for part in content:
|
|
if not isinstance(part, dict) or part.get("type") != "image_url":
|
|
continue
|
|
value = part.get("image_url")
|
|
if isinstance(value, dict):
|
|
value = value.get("url", "")
|
|
if not isinstance(value, str):
|
|
# Bare url key (alternative shape), or nothing resolvable.
|
|
value = part.get("url") if isinstance(part.get("url"), str) else ""
|
|
yield value
|
|
|
|
|
|
def _any_message_has_images(messages: list[Any]) -> bool:
|
|
return any(True for _ in iter_image_url_values(messages))
|