return the bytes when b64_json asks for them

Found by generating a real image against the live server: a request with
"response_format": "b64_json" came back {url, path} and no bytes.

The request model documents OpenAI's values -- url and b64_json -- and every
OpenAI SDK sends the latter. save_image_response compared against this
codebase's own older internal spelling, "base64", so with --file-path
configured the documented behaviour ("if the client explicitly requested
base64, include it") could never fire for a standards-compliant client. It got
a URL, no bytes, and nothing in the response to say why. Nine call sites were
affected: generations, edits, inpaint, upscale, depth, segment and the rest.

Both spellings are accepted now. The url path is unchanged -- the point of
--file-path is not to ship megabytes inline.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
parent b8ae01d5
......@@ -209,12 +209,26 @@ def get_cfg_scale():
return cfg_scale
#: Spellings that mean "give me the bytes inline". ``b64_json`` is the
#: OpenAI-API value and is what every SDK client sends; ``base64`` is this
#: codebase's own older internal name. Both are accepted because the request
#: model documents the first and this function used to test only the second --
#: so a caller asking for base64 the standard way got a URL and no bytes, with
#: nothing to say why. Measured 2026-10-27 against the live server.
_B64_FORMATS = ("base64", "b64_json", "b64")
def _wants_b64(request_format) -> bool:
return str(request_format or "").strip().lower() in _B64_FORMATS
def save_image_response(img, request_format="base64", http_request=None):
"""
Save image to file path if configured, return response dict.
If --file-path is set and request_format is url (not base64), return only URL.
If --file-path is set and request_format is base64, return both URL and base64.
If --file-path is set and request_format is url, return only the URL/path.
If --file-path is set and base64 was asked for (``b64_json`` or
``base64``), return the URL/path AND the bytes.
If --file-path is not set, return base64 as usual.
"""
global global_file_path, global_args
......@@ -242,7 +256,7 @@ def save_image_response(img, request_format="base64", http_request=None):
# If client explicitly requested base64, include it
# Otherwise, only return URL when file-path is set
if request_format == "base64":
if _wants_b64(request_format):
buffered = io.BytesIO()
img.save(buffered, format="PNG")
img_bytes = buffered.getvalue()
......
"""`response_format: "b64_json"` must actually return the bytes.
The request model documents OpenAI's values -- `url` and `b64_json` -- and
every OpenAI SDK sends the latter. `save_image_response` compared against this
codebase's own older internal spelling, `base64`, so when `--file-path` is
configured a client asking for the bytes the standard way got a URL and no
bytes, with nothing in the response to say why. Found by generating a real
image against the live server and getting `{url, path}` back from a request
that asked for b64_json.
"""
import base64
import io
import pytest
from PIL import Image
import codai.api.images as I
@pytest.fixture
def img():
return Image.new("RGB", (8, 8), (10, 120, 200))
@pytest.mark.parametrize("fmt", ["b64_json", "base64", "B64_JSON", " b64_json "])
def test_every_spelling_of_base64_is_honoured(fmt):
assert I._wants_b64(fmt) is True
@pytest.mark.parametrize("fmt", ["url", "URL", None, "", "png"])
def test_url_and_nonsense_are_not_base64(fmt):
assert I._wants_b64(fmt) is False
def test_with_a_file_path_b64_json_returns_both(monkeypatch, tmp_path, img):
monkeypatch.setattr(I, "global_file_path", str(tmp_path))
out = I.save_image_response(img, "b64_json")
assert "url" in out and "path" in out
assert "b64_json" in out, "the bytes the caller explicitly asked for"
# And it is a real PNG, not an empty string.
raw = base64.b64decode(out["b64_json"])
assert Image.open(io.BytesIO(raw)).size == (8, 8)
def test_with_a_file_path_url_returns_no_bytes(monkeypatch, tmp_path, img):
# Unchanged: the point of --file-path is not to ship megabytes inline.
monkeypatch.setattr(I, "global_file_path", str(tmp_path))
out = I.save_image_response(img, "url")
assert "url" in out and "path" in out
assert "b64_json" not in out
def test_without_a_file_path_the_bytes_come_back_either_way(monkeypatch, img):
monkeypatch.setattr(I, "global_file_path", None)
for fmt in ("url", "b64_json"):
out = I.save_image_response(img, fmt)
assert "b64_json" in out, fmt
assert "url" not in out
def test_the_path_is_relative_so_a_caller_need_not_know_the_hostname(
monkeypatch, tmp_path, img):
monkeypatch.setattr(I, "global_file_path", str(tmp_path))
out = I.save_image_response(img, "b64_json")
assert out["path"].startswith("/v1/files/")
def test_the_file_is_actually_written(monkeypatch, tmp_path, img):
import os
monkeypatch.setattr(I, "global_file_path", str(tmp_path))
out = I.save_image_response(img, "url")
name = out["path"].rsplit("/", 1)[-1]
p = os.path.join(str(tmp_path), name)
assert os.path.isfile(p)
assert Image.open(p).size == (8, 8)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment