Skip to content

feat(models): let image blocks set OpenAI's image detail level - #14

Open
poshinchen wants to merge 1 commit into
base-pr-4966from
strandly-v3-pr-4966
Open

poshinchen wants to merge 1 commit into
base-pr-4966from
strandly-v3-pr-4966

Conversation

@poshinchen

@poshinchen poshinchen commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Replica of https://github.com/strands-agents/harness-sdk/pull/4966 (URL in backticks to avoid a cross-reference on the original PR). Branches mirror the original PR's base and head commits.

The review below was produced by the optimized Strandly reviewer (variant v3-reason, optimization run aeb3ee, agent model Opus 4.8) during validation on PRs it never saw in training. Compare with the human and Strandly reviews on the original PR.

Adds an optional detail (low, high, auto) to ImageContent. OpenAIModel, OpenAIResponsesModel (user content and tool results) and the bidi OpenAIRealtimeModel pass it through; payloads without detail are unchanged.

Resolves strands-agents#4531

@poshinchen poshinchen left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strandly v3-reason (optimized) review

Assessment: approve

Assessment: Approve

A focused, well-scoped feature that threads an optional detail through the OpenAI Chat Completions, Responses, and Realtime providers while leaving every other provider's wire shape unchanged. Default payloads are preserved and the backward-compatible rationale (and the deliberate omission of original) is clearly documented.

Review Categories
  • Testing: Strong — covers both Responses image sites, the bidi dict→dataclass round-trip, LiteLLM reuse, and a Bedrock filtering case; uses full-object equality assertions.
  • Documentation: Clear user-facing docs with runnable example and TS parity note.
  • Minor: Inaccurate Raises: docstring on the new Responses _format_input_image; consider whether detail warrants client-side validation vs. relying on the type literal; confirm API-review labeling for the new public type.

Nicely done — the empirical token-count table and byte-identical-payload check give good confidence in the change.

Raises:
ValueError: If the image size exceeds the maximum allowed size.
"""
input_image: dict[str, Any] = {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion

Issue: The _format_input_image docstring says it raises ValueError: If the image size exceeds the maximum allowed size, but the function body does no size validation — it only encodes bytes and conditionally sets detail. This docstring appears copied and is misleading.
Suggestion: Remove the Raises: section (or replace it with an accurate description of any error actually raised, e.g. a KeyError/TypeError if source["bytes"] is absent). Please verify the intended behavior.

}
if "detail" in image:
input_image["detail"] = image["detail"]
return input_image

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion

Issue: The detail value is passed through verbatim to the OpenAI payload without validation. Because ImageContent is a TypedDict, a dict literal with an out-of-range value (e.g. "medium") is not caught at runtime and would reach the API as a request error.
Suggestion: Consider whether a clear client-side error is preferable to a raw API rejection. If relying on the ImageDetail literal for type-checking is the intended contract, that's acceptable — please confirm this is deliberate.

source: The source containing the image's binary content.
detail: The resolution the model processes the image at. Used by `OpenAIModel`, `OpenAIResponsesModel`,
`OpenAIRealtimeModel`, and the providers built on `OpenAIModel`; ignored by other providers.
"""

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion

Issue: This new public API (ImageContent.detail / ImageDetail / ImageBlock.detail) is customer-facing and alters the content-block contract. The PR description doesn't mention an api/needs-review label.
Suggestion: Please confirm the PR carries the api/needs-review label (or equivalent API bar-raiser sign-off) for the new public literal and the optional field, since it extends a widely-used type.

)


def _format_input_image(block: ImageBlock) -> dict[str, Any]:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion

Issue: The two _format_input_image helpers (this one and openai_responses.py) are near-duplicate logic for building an OpenAI input_image part with an optional detail. Duplication risks the two drifting apart over time.
Suggestion: This is acceptable given one operates on an ImageBlock dataclass and the other on an ImageContent dict, but consider a short note or shared helper if a third site appears. Non-blocking.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

Documentation Preview Failed

The documentation deployment encountered an error. Please check the deployment logs for more details.

@poshinchen poshinchen changed the title feat(models): let image blocks set OpenAI's image detail level [replica of harness-sdk#4966] feat(models): let image blocks set OpenAI's image detail level Oct 9, 2026

@poshinchen poshinchen left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strandly v4-reason (optimized) review — run af1746, Opus 4.8, upgraded harness (read_file + suggestion-aware reward)

Assessment: approve

Assessment: Approve

A clean, well-scoped feature that threads an optional detail through the three OpenAI providers while leaving every other provider's wire shape untouched. Defaults are preserved and verified by tests.

Review Categories
  • API Design: Optional NotRequired key + ImageDetail literal is additive and non-breaking; the PR description documents use cases, signatures, exclusions (original), and the TypeScript follow-up. The auto default semantics differ per API but are intentional and documented.
  • Testing: Strong coverage with full-object assertions across all three providers, the bidi path, and a Bedrock filtering test confirming the key never leaks to Converse. One minor redundancy with an older partial-assertion test.
  • Code Quality: Helper extraction (_format_input_image) reduces duplication cleanly. Minor docstring accuracy nit on raised exceptions.

Nicely documented tradeoffs and real-token verification table make this easy to trust.

"""
input_image: dict[str, Any] = {
"type": "input_image",
"image_url": _encode_media_to_data_url(image["source"]["bytes"], image["format"], "image"),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion
Issue: _format_input_image is documented as raising ValueError only "If the image size exceeds the maximum allowed size", but it also raises KeyError on an image missing source.bytes or format (no .get() guards). The behavior is unchanged from the inlined code, so this is non-blocking.
Suggestion: Consider noting the required source["bytes"]/format keys in the docstring to match the Realtime formatter, which explicitly validates and documents its byte requirement.


tru_result = OpenAIResponsesModel._format_request_tool_message(tool_result)
exp_result = {"type": "function_call_output", "call_id": "c2", "output": [exp_image_part]}
assert tru_result == exp_result

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion
Issue: The new parametrized test_format_request_tool_message_with_image_detail asserts the full result object (great), which largely overlaps with the pre-existing test_format_request_tool_message_with_image that only does assert "image_url" in tru_result["output"][1]. The weaker per-field assertion in the old test now silently covers less than the new one.
Suggestion: Consider folding the no-detail case here (already present as the first param) and removing or tightening the older partial-assertion test to avoid redundant, weaker coverage.


format: ImageFormat
source: ImageSource
detail: NotRequired[ImageDetail]

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion
Issue: ImageDetail includes "auto", and the docstring says other providers "ignore it." Since OpenAI's three APIs are the only consumers today, a value like "auto" passed to the Responses/Realtime path is forwarded verbatim, which is correct. No action needed—just confirming the auto semantics differ per API (Chat Completions defaults to it, Responses/Realtime only send when set).
Suggestion: Optionally add a one-line note in the ImageContent.detail docstring clarifying that unset means "provider default" (Chat Completions sends auto; Responses/Realtime send nothing).

@poshinchen

Copy link
Copy Markdown
Owner Author

Live Strandly's actual comments on the original PR 4966 (copied for comparison; mentions escaped and links de-linked so nothing is pinged or cross-referenced)

Review (APPROVED):

Approve. Small, correct passthrough; nothing blocking. One docs nit inline (the OpenAI page says "other providers ignore it" but this PR also makes OpenAIRealtimeModel send it).

Public surface changed (ImageContent.detail, new ImageDetail, ImageBlock.detail) and the PR has no api/needs-review / api/review-complete label. It's an optional field on an existing type, which team/API_BAR_RAISING.md:20 puts in the "minimal — PR review is sufficient" tier, so I'm approving on that basis; a maintainer may still want to label it.

What I verified (head f9b1027)
  • Unit tests for the touched suites: pytest tests/strands/models/test_openai.py test_openai_responses.py test_litellm.py test_bedrock.py test_sagemaker.py tests/strands/types tests/strands/bidi/agent/test_agent.py tests/strands/bidi/models/test_openai.py → 1160 passed. ruff check / ruff format --check clean. mypy did not run cleanly in my sandbox (env issues, not the PR) — relying on CI for that.
  • Tests assert real payloads: test_openai_responses.py:294-316 covers the tool-result path both unset and "high"; test_bedrock.py:2644 proves detail can't leak into Converse; bidi test asserts the websocket frame.
  • No provider passes content["image"] through verbatim — every non-OpenAI formatter rebuilds from format+source (bedrock.py:1057-1068, anthropic.py:218-224, gemini.py:169-173, ollama.py:134, llamaapi.py:129, writer.py:121, llamacpp.py:270). LiteLLMModel/SageMakerAIModel override only video/reasoning and return super().format_request_message_content(content) (litellm.py:126, sagemaker.py:653), so the PR's claim about them holds.
  • bidi/agent/agent.py:419 does ImageBlock(**image); without the new dataclass field a dict with detail would TypeError, so the ImageBlock change is load-bearing and is covered by tests/strands/bidi/agent/test_agent.py:489-505.
  • API shape: per-block detail matches the issue's per-image use case and has a direct precedent in CachePoint.ttl (types/content.py:67-77: optional, provider-specific, docstring names the providers, silently ignored elsewhere). "auto"-explicit (Chat) vs omitted (Responses) is wire-equivalent — all three OpenAI APIs default to auto — and keeps unset payloads byte-identical to main.
Questions (non-blocking)
  • Images the user doesn't construct (MCP tool results, tools/mcp/mcp_client.py:1663,1711) can only get detail via a hook today. An OpenAIConfig-level default (block > config > API default) would cover that and layers on later without breaking anything — fine as a follow-up, not for this PR.
  • The TS ImageBlockData (strands-ts/src/types/media.ts:257-267) has no detail, and the docs now say "Not supported in TypeScript". Worth opening a TS tracking issue so that line has a home; [FEATURE] Let image blocks set OpenAI's image detail level (low/high/auto) strands-agents/harness-sdk#4531 is labelled python only.
Appendix — non-blocking (1)
  • ⚪ strands-py/src/strands/types/media.py:195 — the ImageBlock.detail docstring drops the "used by OpenAI providers; ignored by others" clause that ImageContent.detail (:175-176) has. Harmless; mention only for consistency.

Inline at site/src/content/docs/user-guide/sdk/model-providers/openai.mdx:283:

Docs nit: this PR also makes the bidi OpenAIRealtimeModel send detail (strands-py/src/strands/bidi/models/openai.py:123-124), so "other providers ignore it" is off by one. The ImageContent docstring (types/media.py:175-176) already lists it.

`OpenAIResponsesModel`, `OpenAIRealtimeModel`, `LiteLLMModel`, and `SageMakerAIModel` send
`detail` too. Other providers ignore it.

This branch had an error being deployed

1 failed deployment
auto-approve — f9b10276 Deployed Oct 9, 2026 by poshinchen via Build and Deploy Preview #3
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.

2 participants