fix(llamaswap): use A1111 /sdapi/v1/txt2img so seed is honored #10

Merged
steve merged 1 commits from fix/llamaswap-a1111-seed into main 2026-06-29 03:09:29 +00:00
Owner

sd-server's OpenAI /v1/images/generations endpoint ignores seed on our stable-diffusion.cpp build — every render of a given prompt is byte-identical, so a drawbot batch of N collapses to one image (confirmed live: seeds 111 vs 222 → identical md5 on both SDXL and Qwen-Image).

Switch the image provider to sd-server's A1111 /sdapi/v1/txt2img endpoint, which honors seed (verified live: distinct seeds → distinct images, different md5 and byte sizes). Size is split into width/height; llama-swap still routes by the model field in the body. Optional fields stay pointer/omitempty so unset values fall back to each model's baked --steps/--cfg-scale defaults. Tests + ADR-0016 updated.

sd-server's OpenAI `/v1/images/generations` endpoint **ignores `seed`** on our stable-diffusion.cpp build — every render of a given prompt is byte-identical, so a drawbot batch of N collapses to one image (confirmed live: seeds 111 vs 222 → identical md5 on both SDXL and Qwen-Image). Switch the image provider to sd-server's **A1111 `/sdapi/v1/txt2img`** endpoint, which honors `seed` (verified live: distinct seeds → distinct images, different md5 *and* byte sizes). `Size` is split into `width`/`height`; llama-swap still routes by the `model` field in the body. Optional fields stay pointer/omitempty so unset values fall back to each model's baked `--steps`/`--cfg-scale` defaults. Tests + ADR-0016 updated.
steve added 1 commit 2026-06-29 02:56:50 +00:00
fix(llamaswap): use A1111 /sdapi/v1/txt2img so seed is honored
CI / Tidy (pull_request) Successful in 9m24s
CI / Build & Test (pull_request) Successful in 9m45s
Adversarial Review (Gadfly) / review (pull_request) Successful in 11m30s
a213c18263
The OpenAI /v1/images/generations endpoint ignores `seed` on our
stable-diffusion.cpp build — every render of a given prompt comes back
byte-identical, so a drawbot batch of N collapsed to one image. Switch the
image provider to sd-server's A1111 /sdapi/v1/txt2img endpoint, which honors
`seed` (verified live: distinct seeds -> distinct images on SDXL and
Qwen-Image). Size is split into width/height; llama-swap still routes by the
`model` field. Tests + ADR-0016 updated.

🪰 Gadfly — live review status

7/7 reviewers finished · updated 2026-06-29 03:08:20Z

claude-code/opus · claude-code — done

  • security — No material issues found
  • correctness — Minor issues
  • maintainability — Minor issues
  • performance — No material issues found
  • error-handling — No material issues found

claude-code/opus:max · claude-code — done

  • security — No material issues found
  • correctness — Minor issues
  • maintainability — Minor issues
  • performance — No material issues found
  • error-handling — No material issues found

deepseek-v4-pro:cloud · ollama-cloud — done

  • security — No material issues found
  • correctness — No material issues found
  • maintainability — Minor issues
  • performance — No material issues found
  • error-handling — No material issues found

devstral-2:123b-cloud · ollama-cloud — done

  • security — No material issues found
  • correctness — Minor issues
  • maintainability — Minor issues
  • performance — No material issues found
  • error-handling — Minor issues

glm-5.2:cloud · ollama-cloud — done

  • security — No material issues found
  • correctness — Minor issues
  • maintainability — Minor issues
  • performance — No material issues found
  • error-handling — Minor issues

kimi-k2.6:cloud · ollama-cloud — done

  • security — No material issues found
  • correctness — No material issues found
  • maintainability — Minor issues
  • performance — No material issues found
  • error-handling — No material issues found

qwen3.5:397b-cloud · ollama-cloud — done

  • security — Minor issues
  • correctness — No material issues found
  • maintainability — No material issues found
  • performance — No material issues found
  • error-handling — Minor issues

Live status board. Findings are posted in each model's own comment. Advisory only — does not block merge.

<!-- gadfly-status-board --> ## 🪰 Gadfly — live review status 7/7 reviewers finished · updated 2026-06-29 03:08:20Z #### `claude-code/opus` · claude-code — ✅ done - ✅ **security** — No material issues found - ✅ **correctness** — Minor issues - ✅ **maintainability** — Minor issues - ✅ **performance** — No material issues found - ✅ **error-handling** — No material issues found #### `claude-code/opus:max` · claude-code — ✅ done - ✅ **security** — No material issues found - ✅ **correctness** — Minor issues - ✅ **maintainability** — Minor issues - ✅ **performance** — No material issues found - ✅ **error-handling** — No material issues found #### `deepseek-v4-pro:cloud` · ollama-cloud — ✅ done - ✅ **security** — No material issues found - ✅ **correctness** — No material issues found - ✅ **maintainability** — Minor issues - ✅ **performance** — No material issues found - ✅ **error-handling** — No material issues found #### `devstral-2:123b-cloud` · ollama-cloud — ✅ done - ✅ **security** — No material issues found - ✅ **correctness** — Minor issues - ✅ **maintainability** — Minor issues - ✅ **performance** — No material issues found - ✅ **error-handling** — Minor issues #### `glm-5.2:cloud` · ollama-cloud — ✅ done - ✅ **security** — No material issues found - ✅ **correctness** — Minor issues - ✅ **maintainability** — Minor issues - ✅ **performance** — No material issues found - ✅ **error-handling** — Minor issues #### `kimi-k2.6:cloud` · ollama-cloud — ✅ done - ✅ **security** — No material issues found - ✅ **correctness** — No material issues found - ✅ **maintainability** — Minor issues - ✅ **performance** — No material issues found - ✅ **error-handling** — No material issues found #### `qwen3.5:397b-cloud` · ollama-cloud — ✅ done - ✅ **security** — Minor issues - ✅ **correctness** — No material issues found - ✅ **maintainability** — No material issues found - ✅ **performance** — No material issues found - ✅ **error-handling** — Minor issues <sub>Live status board. Findings are posted in each model's own comment. Advisory only — does not block merge.</sub>

🪰 Gadfly review — claude-code/opus (claude-code)

Verdict: Minor issues — 5 reviewers: security, correctness, maintainability, performance, error-handling

🔒 Security — No material issues found

I've verified the full request/response path. Here's my security review.


VERDICT: No material issues found

I traced the change through the Security lens against the checked-out code:

  • No injection / no URL tampering. The endpoint path is the hardcoded constant "/sdapi/v1/txt2img" (image.go:85), and the request body is built as a typed struct and json.Marshal-ed in doJSON (llamaswap.go:194). User/agent-controlled fields (Prompt, NegativePrompt, Model=m.id) ride in the JSON body where the encoder escapes them — none of them reach the URL path or a shell. Verified by reading doJSON (llamaswap.go:188-227).
  • No SSRF. p.baseURL comes from provider config (WithBaseURL/LLM_* DSN), not from per-request untrusted input, and path is a literal. Request target = p.baseURL+path with a fixed suffix.
  • Untrusted response handling is bounded and safe. resp.Images comes from the upstream sd-server, but the response is decoded through io.LimitReader(resp.Body, maxResponseBytes) (llamaswap.go:219), and each base64 entry is decoded with base64.StdEncoding.DecodeString with the error surfaced (image.go:94-97). The new continue-on-empty (image.go:91) only changes which-entry behavior, not a security boundary. MIME is sniffed locally, not trusted from the wire (sniffImageMIME).
  • Input validation on the new parseSize. Width/height are parsed with strconv.Atoi and rejected if non-numeric or <= 0 (image.go:121-124); empty input yields (nil,nil). No negative/zero values reach the wire. There's no explicit upper bound, but oversized dimensions are a resource concern for the downstream renderer (performance lens), not an injection/auth issue — and the value is still just a JSON integer.
  • No secret/credential changes. The Authorization: Bearer header logic in doJSON is untouched; nothing new is logged or echoed.

Nothing in the authn/authz, injection, SSRF, deserialization, or secret-leakage areas is materially wrong in this diff.

🎯 Correctness — Minor issues

I've verified both findings against the actual source.

Finding 1 (batch_count external contract): The draft itself concedes this is "Unverified — external HTTP contract, not checkable from the repo." It cannot be positively confirmed against the code. Moreover, its supporting argument is misleading: the target is stable-diffusion.cpp's sd-server, not stock AUTOMATIC1111, and batch_count is in fact the field sd-server's A1111-compatible handler accepts. This is speculation about external backend behavior, not a defect confirmable from the repo. Dropped.

Finding 2 (under-count silently accepted): Verified by reading image.go:90-107 — the loop appends only non-empty entries and errors only when len(out.Images) == 0. Factually accurate and confirmed.


VERDICT: Minor issues

Findings (Correctness lens)

  • Under-count is silently accepted. provider/llamaswap/image.go:90-107 only errors when the result contains zero images (len(out.Images) == 0). A caller requesting N images that gets back fewer than N receives a successful Result with too few images and no error. This property existed on the prior OpenAI path too, so the diff doesn't introduce it, but the switch to A1111 batch_count makes a collapsed/partial batch a more plausible outcome. Consider asserting the returned count against the requested N when N>1. (Note: imagegen.Request documents N loosely — N==0 means "provider default, usually one" — so this is a defensive-validation suggestion, not a contract violation.)

Checked and clean

  • parseSize (image.go:112-127): correct — "WxH" split, ToLower handles 512X512, rejects non-positive/non-numeric, and ""(nil, nil) so the model's baked resolution applies. Consistent with imagegen.Request.Size semantics (imagegen/imagegen.go:38-40).
  • Seed handling genuinely changes behavior: Seed *int64 is forwarded (omitempty) to an endpoint that honors it; nil omits → backend random. Consistent with the stated fix.
  • Optional fields are correctly pointer/omitempty; the endpoint path is /sdapi/v1/txt2img matching the new path and the updated tests.
  • txt2imgResponse reading only images is fine; extra A1111 fields (info/parameters) are harmlessly ignored.
🧹 Code cleanliness & maintainability — Minor issues

I've verified all three findings against the actual source. Findings 1 and 2 are confirmed verbatim. Finding 3 is not a confirmable defect — the comment reads "pointers/omitempty," which already parses as "pointer fields or omitempty value fields," so it is not actually inaccurate; I'm dropping it.


VERDICT: Minor issues

  • Stale package doc now contradicts the implementationimagegen/imagegen.go:13-14 still reads "The first implementation is provider/llamaswap, which targets the OpenAI /v1/images/generations endpoint routed to a stable-diffusion.cpp backend." This PR is the change that makes that false: the code now POSTs /sdapi/v1/txt2img (provider/llamaswap/image.go:56,85). CLAUDE.md makes "docs match reality in the same commit that changes behavior" a credibility promise; the PR updated ADR-0016 but missed this package-level comment. Fix: update the sentence to reference the A1111 /sdapi/v1/txt2img endpoint (or drop the endpoint specifics, since it's a leaky implementation detail in a contract-package doc).

  • Trivial: duplicated error literal in parseSizeprovider/llamaswap/image.go:119 and :124 repeat fmt.Errorf("invalid size %q (want WxH)", size) verbatim. Collapsing the two failure paths into one shared error removes the copy-paste. Low priority.

Notes:

  • I checked for an existing WxH/size parser to reuse across imagegen/, provider/, and media/; parseSize is the only one, so introducing it locally is reasonable — no duplication of an existing utility.
  • The new parseSize helper, naming, and field reordering otherwise read cleanly. The ADR change uses the append-only "Update" section, which is the repo's documented convention — correct, not a nit.
  • The empty-b64 continue-then-error-if-all-empty change (image.go:90-106) is an error-handling/behavior shift left to the error-handling lens.
Performance — No material issues found

I've verified the full changed file. This is a like-for-like endpoint/shape swap; let me assess it strictly for performance.

VERDICT: No material issues found

Performance-lens assessment (verified by reading provider/llamaswap/image.go at the PR checkout):

  • Per-call work is unchanged in shape. Generate still does one doJSON round-trip (now to /sdapi/v1/txt2img) and one pass over the returned images. No new network calls, no N+1, no added round-trips. image.go:84-87
  • parseSize is O(1) and runs once per request — a single SplitN(..., 2) and two Atoi. Not on any loop or hot path. image.go:112-127
  • Response loop is bounded by the image count the caller requests (req.N, validated >= 0), same as before. Base64 decode + MIME sniff per image is inherent to returning bytes, not a regression. image.go:90-99
  • BatchCount: req.N simply relabels the previously-sent N field; no change to how much work is requested upstream. Any per-image generation cost is the intended behavior of the fix (distinct seeds now actually produce distinct renders), not avoidable overhead introduced here.

Minor non-issue I considered and am not flagging as material: out.Images is appended without a pre-sized make(..., 0, len(resp.Images)). With image batches being small (single digits typically) and dwarfed by GPU render + base64 decode cost, the reallocations are negligible — a true micro-optimization, below the bar.

Nothing in the performance lens is materially wrong here.

🧯 Error handling & edge cases — No material issues found

I've verified the actual checked-out code, which includes lines the truncated diff hunk didn't show (the post-loop len(out.Images) == 0 guard at image.go:100-106). That changes the analysis materially, so reading the file was necessary.

VERDICT: No material issues found

Reviewed through the error-handling & edge-cases lens. The unhappy paths this diff introduces are handled well:

  • Empty/whitespace prompt (image.go:59) and negative N (image.go:62) still rejected up front — unchanged, good.
  • parseSize edge cases (image.go:112-127) — verified robust:
    • ""(nil, nil, nil), correctly falling back to model default.
    • Non-WxH input ("512", "512x512x512" → SplitN gives "512x512" which Atoi rejects, "512x" → empty part) all produce a clean ErrUnsupported error rather than a panic or silent garbage.
    • Zero/negative dims caught by w <= 0 || h <= 0; integer overflow caught by strconv.Atoi returning err1/err2. No off-by-one or panic surface.
  • All-empty image response is handled, not swallowed (image.go:100-106): the loop now continues past empty b64 strings, but the post-loop len(out.Images) == 0 guard returns an APIError. So a response of {"images":[]} or {"images":[""]} fails loudly rather than returning a hollow success. This was not visible in the diff hunk but is the real safety net replacing the old per-entry error — confirmed by reading the file.
  • Decode failure (image.go:94-97) still returns a wrapped error per image; index i is the slice index, correct.

One deliberate behavior change worth being aware of (not a defect): partial-empty responses are now tolerated — if the backend returns N slots with some empty, the caller silently gets fewer than N images instead of an error (old code errored on the first empty entry). For A1111 batch_count this is defensible defensive handling, and the all-empty case is still an error, so I don't consider it a material edge-case bug.

No ignored errors, no missing cleanup (no resources to release here — doJSON owns the HTTP body), and no panic-on-bad-input paths in the changed code.

Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 2m 46s

<!-- gadfly-review:ollama:claude-code/opus --> ### 🪰 Gadfly review — `claude-code/opus` (claude-code) **Verdict: Minor issues** — 5 reviewers: security, correctness, maintainability, performance, error-handling <details><summary><b>🔒 Security</b> — No material issues found</summary> I've verified the full request/response path. Here's my security review. --- **VERDICT: No material issues found** I traced the change through the Security lens against the checked-out code: - **No injection / no URL tampering.** The endpoint path is the hardcoded constant `"/sdapi/v1/txt2img"` (`image.go:85`), and the request body is built as a typed struct and `json.Marshal`-ed in `doJSON` (`llamaswap.go:194`). User/agent-controlled fields (`Prompt`, `NegativePrompt`, `Model`=`m.id`) ride in the JSON body where the encoder escapes them — none of them reach the URL path or a shell. Verified by reading `doJSON` (`llamaswap.go:188-227`). - **No SSRF.** `p.baseURL` comes from provider config (`WithBaseURL`/`LLM_*` DSN), not from per-request untrusted input, and `path` is a literal. Request target = `p.baseURL+path` with a fixed suffix. - **Untrusted response handling is bounded and safe.** `resp.Images` comes from the upstream sd-server, but the response is decoded through `io.LimitReader(resp.Body, maxResponseBytes)` (`llamaswap.go:219`), and each base64 entry is decoded with `base64.StdEncoding.DecodeString` with the error surfaced (`image.go:94-97`). The new `continue`-on-empty (`image.go:91`) only changes which-entry behavior, not a security boundary. MIME is sniffed locally, not trusted from the wire (`sniffImageMIME`). - **Input validation on the new `parseSize`.** Width/height are parsed with `strconv.Atoi` and rejected if non-numeric or `<= 0` (`image.go:121-124`); empty input yields `(nil,nil)`. No negative/zero values reach the wire. There's no explicit upper bound, but oversized dimensions are a resource concern for the downstream renderer (performance lens), not an injection/auth issue — and the value is still just a JSON integer. - **No secret/credential changes.** The `Authorization: Bearer` header logic in `doJSON` is untouched; nothing new is logged or echoed. Nothing in the authn/authz, injection, SSRF, deserialization, or secret-leakage areas is materially wrong in this diff. </details> <details><summary><b>🎯 Correctness</b> — Minor issues</summary> I've verified both findings against the actual source. **Finding 1 (`batch_count` external contract):** The draft itself concedes this is "Unverified — external HTTP contract, not checkable from the repo." It cannot be positively confirmed against the code. Moreover, its supporting argument is misleading: the target is stable-diffusion.cpp's `sd-server`, not stock AUTOMATIC1111, and `batch_count` is in fact the field sd-server's A1111-compatible handler accepts. This is speculation about external backend behavior, not a defect confirmable from the repo. **Dropped.** **Finding 2 (under-count silently accepted):** Verified by reading `image.go:90-107` — the loop appends only non-empty entries and errors only when `len(out.Images) == 0`. Factually accurate and confirmed. --- **VERDICT: Minor issues** ### Findings (Correctness lens) - **Under-count is silently accepted.** `provider/llamaswap/image.go:90-107` only errors when the result contains **zero** images (`len(out.Images) == 0`). A caller requesting `N` images that gets back fewer than `N` receives a successful `Result` with too few images and no error. This property existed on the prior OpenAI path too, so the diff doesn't introduce it, but the switch to A1111 `batch_count` makes a collapsed/partial batch a more plausible outcome. Consider asserting the returned count against the requested `N` when `N>1`. *(Note: `imagegen.Request` documents `N` loosely — `N==0` means "provider default, usually one" — so this is a defensive-validation suggestion, not a contract violation.)* ### Checked and clean - `parseSize` (`image.go:112-127`): correct — `"WxH"` split, `ToLower` handles `512X512`, rejects non-positive/non-numeric, and `""` → `(nil, nil)` so the model's baked resolution applies. Consistent with `imagegen.Request.Size` semantics (`imagegen/imagegen.go:38-40`). - Seed handling genuinely changes behavior: `Seed *int64` is forwarded (omitempty) to an endpoint that honors it; nil omits → backend random. Consistent with the stated fix. - Optional fields are correctly pointer/omitempty; the endpoint path is `/sdapi/v1/txt2img` matching the new path and the updated tests. - `txt2imgResponse` reading only `images` is fine; extra A1111 fields (`info`/`parameters`) are harmlessly ignored. </details> <details><summary><b>🧹 Code cleanliness & maintainability</b> — Minor issues</summary> I've verified all three findings against the actual source. Findings 1 and 2 are confirmed verbatim. Finding 3 is not a confirmable defect — the comment reads "pointers/omitempty," which already parses as "pointer fields or omitempty value fields," so it is not actually inaccurate; I'm dropping it. --- VERDICT: Minor issues - **Stale package doc now contradicts the implementation** — `imagegen/imagegen.go:13-14` still reads *"The first implementation is provider/llamaswap, which targets the OpenAI `/v1/images/generations` endpoint routed to a stable-diffusion.cpp backend."* This PR is the change that makes that false: the code now POSTs `/sdapi/v1/txt2img` (`provider/llamaswap/image.go:56,85`). CLAUDE.md makes "docs match reality in the same commit that changes behavior" a credibility promise; the PR updated ADR-0016 but missed this package-level comment. Fix: update the sentence to reference the A1111 `/sdapi/v1/txt2img` endpoint (or drop the endpoint specifics, since it's a leaky implementation detail in a contract-package doc). - **Trivial: duplicated error literal in `parseSize`** — `provider/llamaswap/image.go:119` and `:124` repeat `fmt.Errorf("invalid size %q (want WxH)", size)` verbatim. Collapsing the two failure paths into one shared error removes the copy-paste. Low priority. Notes: - I checked for an existing WxH/size parser to reuse across `imagegen/`, `provider/`, and `media/`; `parseSize` is the only one, so introducing it locally is reasonable — no duplication of an existing utility. - The new `parseSize` helper, naming, and field reordering otherwise read cleanly. The ADR change uses the append-only "Update" section, which is the repo's documented convention — correct, not a nit. - The empty-`b64` `continue`-then-error-if-all-empty change (`image.go:90-106`) is an error-handling/behavior shift left to the error-handling lens. </details> <details><summary><b>⚡ Performance</b> — No material issues found</summary> I've verified the full changed file. This is a like-for-like endpoint/shape swap; let me assess it strictly for performance. **VERDICT: No material issues found** Performance-lens assessment (verified by reading `provider/llamaswap/image.go` at the PR checkout): - **Per-call work is unchanged in shape.** `Generate` still does one `doJSON` round-trip (now to `/sdapi/v1/txt2img`) and one pass over the returned images. No new network calls, no N+1, no added round-trips. `image.go:84-87` - **`parseSize` is O(1) and runs once per request** — a single `SplitN(..., 2)` and two `Atoi`. Not on any loop or hot path. `image.go:112-127` - **Response loop is bounded by the image count** the caller requests (`req.N`, validated `>= 0`), same as before. Base64 decode + MIME sniff per image is inherent to returning bytes, not a regression. `image.go:90-99` - **`BatchCount: req.N`** simply relabels the previously-sent `N` field; no change to how much work is requested upstream. Any per-image generation cost is the intended behavior of the fix (distinct seeds now actually produce distinct renders), not avoidable overhead introduced here. Minor non-issue I considered and am **not** flagging as material: `out.Images` is appended without a pre-sized `make(..., 0, len(resp.Images))`. With image batches being small (single digits typically) and dwarfed by GPU render + base64 decode cost, the reallocations are negligible — a true micro-optimization, below the bar. Nothing in the performance lens is materially wrong here. </details> <details><summary><b>🧯 Error handling & edge cases</b> — No material issues found</summary> I've verified the actual checked-out code, which includes lines the truncated diff hunk didn't show (the post-loop `len(out.Images) == 0` guard at `image.go:100-106`). That changes the analysis materially, so reading the file was necessary. ## VERDICT: No material issues found Reviewed through the **error-handling & edge-cases** lens. The unhappy paths this diff introduces are handled well: - **Empty/whitespace prompt** (`image.go:59`) and **negative N** (`image.go:62`) still rejected up front — unchanged, good. - **`parseSize` edge cases** (`image.go:112-127`) — verified robust: - `""` → `(nil, nil, nil)`, correctly falling back to model default. - Non-`WxH` input (`"512"`, `"512x512x512"` → SplitN gives `"512x512"` which `Atoi` rejects, `"512x"` → empty part) all produce a clean `ErrUnsupported` error rather than a panic or silent garbage. - Zero/negative dims caught by `w <= 0 || h <= 0`; integer overflow caught by `strconv.Atoi` returning `err1`/`err2`. No off-by-one or panic surface. - **All-empty image response is handled, not swallowed** (`image.go:100-106`): the loop now `continue`s past empty `b64` strings, but the post-loop `len(out.Images) == 0` guard returns an `APIError`. So a response of `{"images":[]}` or `{"images":[""]}` fails loudly rather than returning a hollow success. This was not visible in the diff hunk but is the real safety net replacing the old per-entry error — confirmed by reading the file. - **Decode failure** (`image.go:94-97`) still returns a wrapped error per image; index `i` is the slice index, correct. One deliberate behavior change worth being aware of (not a defect): partial-empty responses are now tolerated — if the backend returns N slots with some empty, the caller silently gets fewer than N images instead of an error (old code errored on the first empty entry). For A1111 `batch_count` this is defensible defensive handling, and the all-empty case is still an error, so I don't consider it a material edge-case bug. No ignored errors, no missing cleanup (no resources to release here — `doJSON` owns the HTTP body), and no panic-on-bad-input paths in the changed code. </details> <sub>Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 2m 46s</sub>

🪰 Gadfly review — glm-5.2:cloud (ollama-cloud)

Verdict: Minor issues — 5 reviewers: security, correctness, maintainability, performance, error-handling

🔒 Security — No material issues found

Verdict: No material issues found

I traced the change end-to-end through the security lens and verified each tainted input path against the checked-out code.

  • Request construction (image.go:71-82): Prompt, NegativePrompt, Sampler, Seed, Steps, CFGScale are all placed into a struct and marshaled by json.Marshal inside doJSON (llamaswap.go:194). No string interpolation into the URL, body, or a query — so no injection surface. The endpoint path /sdapi/v1/txt2img is a hardcoded constant, not user-derived, so no path traversal/SSF vector is introduced by the endpoint switch.

  • model field routing (image.go:73): m.id (caller-supplied at ImageModel) is sent only as a JSON body field, never interpolated into the URL path. Verified the URL is built purely as p.baseURL+path (llamaswap.go:200) with path constant. No route-injection.

  • Size parsing (image.go:112-127): new parseSize validates with strconv.Atoi (digits-only) and rejects w <= 0 || h <= 0, so only positive integers reach width/height. Malformed sizes return ErrUnsupported rather than being passed through. This is stricter than the prior code, which forwarded an opaque Size string. Good.

  • Auth (llamaswap.go:207-209): the bearer token is applied to the new endpoint exactly as before; no authn/authz regression from the endpoint change.

  • Response handling (image.go:90-98, llamaswap.go:219): responses are read via io.LimitReader(resp.Body, maxResponseBytes) (64 MiB cap, llamaswap.go:44), decoded with encoding/json into a plain struct (no custom UnmarshalJSON), then base64.StdEncoding.DecodeString. No unsafe deserialization, no second fetch of remote url content (the old URL-only branch was removed entirely), and sniffImageMIME uses http.DetectContentType on the decoded bytes. No execution/reflective path.

  • batch_count (image.go:81): req.N is validated >= 0 at image.go:62; with omitempty, N=0 omits the field (backend default), N>0 forwards it. No unbounded/amplification-by-attacker issue beyond the existing caller-controlled contract.

No security-relevant problems in this PR.

🎯 Correctness — Minor issues

VERDICT: Minor issues

  • provider/llamaswap/image.go:81BatchCount: req.N combined with a single fixed Seed (image.go:75) could re-collapse a batch to identical images, depending on sd-server backend behavior not present in this repo. The code is confirmed: one txt2imgRequest sends one Seed *int64 plus BatchCount int = req.N, so a caller that sets both WithSeed(s) and WithN(k>1) issues a single request with a fixed seed and a batch count. The PR's justification (the quoted live test) only covers the single-image, distinct-seed-per-request case and does not demonstrate the batch_count > 1 + fixed-seed case. Whether stable-diffusion.cpp's /sdapi/v1/txt2img varies noise per batch image or applies the literal seed to every image cannot be determined from this repository. This is not a confirmed correctness bug, but a real semantic gap worth either documenting or guarding against (e.g., when req.N > 1 and req.Seed != nil, issue N separate requests with seed, seed+1, …). Suggested: add a test asserting the intended semantics, or document the constraint. Minor / unverified-impact.

  • provider/llamaswap/image.go:90-106 — no check that the decoded-image count matches req.N (pre-existing, not introduced by this PR). The loop (image.go:90-99) iterates resp.Images, skips empty base64 strings, and appends; the only error path is len(out.Images) == 0 (image.go:100). If the backend silently under-delivers (e.g., ignores batch_count and returns 1 image for an N=4 request), the caller receives a short result with no error. Confirmed in the current code; this lenient behavior predates the PR, but the switch from the OpenAI n field to batch_count (a field whose honoring by this sd-server build is not verified in-repo) makes the silent-under-delivery path more plausible. Not blocking.

  • provider/llamaswap/image.go:68fmt.Errorf("%w: %v", llm.ErrUnsupported, err) wraps the outer sentinel with %w but renders the inner parseSize error with %v, so the parse error is not unwrappable. Confirmed: parseSize returns fmt.Errorf("invalid size %q (want WxH)", size) errors, and Generate formats them via %v. Material impact is negligible (the inner error is a formatted string with no further wrapping), but for consistency the inner error should use %w as well. Minor.

🧹 Code cleanliness & maintainability — Minor issues

All four findings are verifiable against the actual code. Let me confirm each:

Finding 1 (image.go:90-107): Confirmed. Old code returned an explicit *llm.APIError per-image with message "image %d returned no inline b64_json data" and a comment explaining the rationale. New code (lines 90-92) silently continues on empty string, only reporting generic "image response contained no images" (line 104) if all empty. The "Why error rather than skip" comment is dropped with no replacement.

Finding 2 (image.go:117): Confirmed. parseSize calls strings.ToLower(size) before splitting on "x", enabling "512X512". Doc comment (line 110-111) says only "WxH" — case-insensitive separator not documented. The ToLower only matters for uppercase separator X.

Finding 3 (ADR lines 33-35): Confirmed. "First implementation" bullet still states OpenAI /v1/images/generations with response_format: "b64_json", now historically inaccurate given the appended Update section (lines 56-63). Not marked superseded.

Finding 4 (image.go:49): Confirmed. Struct comment (lines 31-38) describes pointers/omitempty as the unset-default mechanism. BatchCount int (line 49) with omitempty uses zero-value semantics (0 = backend default) differently from pointer fields, but this isn't called out. imagegen.Request.N (imagegen.go:35-36) confirms N == 0 = provider default. All findings survive verification.

All four findings are cleanliness/maintainability nits (explicitly the reviewer's lens), not correctness blockers. The verdict "Minor issues" is appropriate.

Corrected review

VERDICT: Minor issues

  • provider/llamaswap/image.go:90-107 — The old code surfaced an explicit *llm.APIError for a b64_json-less entry with a helpful "image %d returned no inline b64_json data" message; the new code silently continues on an empty string and only later reports a generic "image response contained no images" (line 104) if all entries were empty. The comment explaining the prior "Why error rather than skip" rationale was dropped with no replacement. From a cleanliness standpoint this is a regression in self-documentation: a reader can no longer tell why empty entries are tolerated vs. flagged. Consider either restoring a brief comment (e.g. // A1111 may return empty slots; skip them, but error if none decode) or keeping the per-image error. Verified by reading the full Generate body (image.go:90-107).

  • provider/llamaswap/image.go:117parseSize calls strings.ToLower(size) before splitting on "x", which is only meaningful if the separator could be uppercase "X" (e.g. "512X512"); strconv.Atoi handles digits/sign case-insensitively already. That's a real, harmless input-tolerance nicety, but it's undocumented in the function's doc comment (image.go:110-111), which says only "WxH". Minor: either mention "WxH" is case-insensitive on the separator, or drop the ToLower if you don't intend to accept "X".

  • docs/adr/0016-imagegen-interface.md:33-35 — The ADR's original "First implementation" bullet still states the provider targets OpenAI /v1/images/generations with response_format: "b64_json", which is now historically inaccurate. The new "Update — A1111 txt2img endpoint" section is appended below (lines 56-63), leaving a contradictory in-place claim. For maintainability the stale bullet should be marked superseded (e.g. ~~First implementation: OpenAI /v1/images/generations~~ (superseded; see Update below)), consistent with how the doc already layers "Update" sections.

  • provider/llamaswap/image.go:49BatchCount int uses omitempty, and req.N == 0 (the documented "provider default" sentinel per imagegen.Request.N, imagegen.go:35-36) correctly maps to batch_count being omitted. However the struct comment (lines 31-38) describes pointer/omitempty fields as the unset-default mechanism and then lists BatchCount as a plain int with omitempty without calling out that its zero-value semantics differ (0 = backend default, not "unset"). Minor readability nit: a one-line note that N/batch_count uses zero-as-default rather than pointer-as-default would keep the comment honest.

Performance — No material issues found

VERDICT: No material issues found

Reviewed through the performance lens. The change is a wire-format swap (OpenAI → A1111), not an algorithmic change:

  • parseSize (image.go:112) runs once per Generate call and does a single SplitN + two Atoi — negligible, not on a hot loop.
  • Base64 decode loop (image.go:90-98) is unchanged in shape: it still iterates over the response images and decodes each once. No new allocations or quadratic behavior introduced.
  • BatchCount: req.N (image.go:81) maps the request count 1:1 to batch_count; the server generates N images in one request rather than the client issuing N requests, so this is if anything more efficient than a loop.
  • No new unbounded growth, blocking calls, or pagination concerns.

Nothing material to flag in the performance lane.

🧯 Error handling & edge cases — Minor issues

VERDICT: Minor issues

  • provider/llamaswap/image.go:62-64,81 — The guard rejects req.N < 0 but permits req.N == 0. With the new A1111 wire shape, BatchCount is a plain int with omitempty (line 49), so N == 0 maps to BatchCount: 0 (line 81) and the field is omitted from the request body entirely. The imagegen.Request contract (imagegen/imagegen.go:29,35) explicitly promises "N == 0 yields the backend's default count (usually one)". Whether sd-server's /sdapi/v1/txt2img treats an omitted batch_count as 1 (default count) or 0 is not determinable from this repo; if it defaults to 0, the code's own safety net at line 100 (len(out.Images) == 0"image response contained no images" APIError) turns a legitimate "default count" request into a hard error. Suggested fix: map req.N == 0 to BatchCount: 1, or document that callers must set N explicitly for this provider. (Backend default unverified — no sd-server source in repo.)

  • provider/llamaswap/image.go:90-93 — A returned-but-empty image slot (b64 "") is now silently continued. The previous code explicitly returned a hard APIError ("image %d returned no inline b64_json data") for an empty entry, with a comment explaining the choice. If sd-server returns a mix of populated and empty strings (e.g. a partial mid-batch failure that yields an empty element), the caller now gets a silently-shortened result with no indication that image i was dropped; an error only surfaces if the whole batch collapses to empty (line 100). This is a regression in partial-failure visibility. Suggested fix: count skipped entries and surface that in the result/error, or treat a non-empty resp.Images array containing any empty string as a partial-failure error as before.

  • provider/llamaswap/image.go:66-69parseSize failure is wrapped with llm.ErrUnsupported. A malformed Size is a caller-input bug rather than an unsupported-capability condition, so the error category is semantically off; this is consistent with the file's existing use of ErrUnsupported for the N and prompt guards, so the mismatch is preserved rather than introduced. Note only; no action required.

Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 3m 10s

<!-- gadfly-review:ollama:glm-5.2:cloud --> ### 🪰 Gadfly review — `glm-5.2:cloud` (ollama-cloud) **Verdict: Minor issues** — 5 reviewers: security, correctness, maintainability, performance, error-handling <details><summary><b>🔒 Security</b> — No material issues found</summary> **Verdict: No material issues found** I traced the change end-to-end through the security lens and verified each tainted input path against the checked-out code. - **Request construction** (`image.go:71-82`): `Prompt`, `NegativePrompt`, `Sampler`, `Seed`, `Steps`, `CFGScale` are all placed into a struct and marshaled by `json.Marshal` inside `doJSON` (`llamaswap.go:194`). No string interpolation into the URL, body, or a query — so no injection surface. The endpoint path `/sdapi/v1/txt2img` is a hardcoded constant, not user-derived, so no path traversal/SSF vector is introduced by the endpoint switch. - **`model` field routing** (`image.go:73`): `m.id` (caller-supplied at `ImageModel`) is sent only as a JSON body field, never interpolated into the URL path. Verified the URL is built purely as `p.baseURL+path` (`llamaswap.go:200`) with `path` constant. No route-injection. - **`Size` parsing** (`image.go:112-127`): new `parseSize` validates with `strconv.Atoi` (digits-only) and rejects `w <= 0 || h <= 0`, so only positive integers reach `width`/`height`. Malformed sizes return `ErrUnsupported` rather than being passed through. This is stricter than the prior code, which forwarded an opaque `Size` string. Good. - **Auth** (`llamaswap.go:207-209`): the bearer token is applied to the new endpoint exactly as before; no authn/authz regression from the endpoint change. - **Response handling** (`image.go:90-98`, `llamaswap.go:219`): responses are read via `io.LimitReader(resp.Body, maxResponseBytes)` (64 MiB cap, `llamaswap.go:44`), decoded with `encoding/json` into a plain struct (no custom `UnmarshalJSON`), then `base64.StdEncoding.DecodeString`. No unsafe deserialization, no second fetch of remote `url` content (the old URL-only branch was removed entirely), and `sniffImageMIME` uses `http.DetectContentType` on the decoded bytes. No execution/reflective path. - **`batch_count`** (`image.go:81`): `req.N` is validated `>= 0` at `image.go:62`; with `omitempty`, `N=0` omits the field (backend default), `N>0` forwards it. No unbounded/amplification-by-attacker issue beyond the existing caller-controlled contract. No security-relevant problems in this PR. </details> <details><summary><b>🎯 Correctness</b> — Minor issues</summary> **VERDICT: Minor issues** - **`provider/llamaswap/image.go:81` — `BatchCount: req.N` combined with a single fixed `Seed` (`image.go:75`) could re-collapse a batch to identical images, depending on sd-server backend behavior not present in this repo.** The code is confirmed: one `txt2imgRequest` sends one `Seed *int64` plus `BatchCount int = req.N`, so a caller that sets both `WithSeed(s)` and `WithN(k>1)` issues a single request with a fixed seed and a batch count. The PR's justification (the quoted live test) only covers the single-image, distinct-seed-per-request case and does not demonstrate the `batch_count > 1` + fixed-seed case. Whether stable-diffusion.cpp's `/sdapi/v1/txt2img` varies noise per batch image or applies the literal seed to every image cannot be determined from this repository. This is not a confirmed correctness bug, but a real semantic gap worth either documenting or guarding against (e.g., when `req.N > 1` and `req.Seed != nil`, issue N separate requests with `seed, seed+1, …`). Suggested: add a test asserting the intended semantics, or document the constraint. Minor / unverified-impact. - **`provider/llamaswap/image.go:90-106` — no check that the decoded-image count matches `req.N` (pre-existing, not introduced by this PR).** The loop (`image.go:90-99`) iterates `resp.Images`, skips empty base64 strings, and appends; the only error path is `len(out.Images) == 0` (`image.go:100`). If the backend silently under-delivers (e.g., ignores `batch_count` and returns 1 image for an `N=4` request), the caller receives a short result with no error. Confirmed in the current code; this lenient behavior predates the PR, but the switch from the OpenAI `n` field to `batch_count` (a field whose honoring by this sd-server build is not verified in-repo) makes the silent-under-delivery path more plausible. Not blocking. - **`provider/llamaswap/image.go:68` — `fmt.Errorf("%w: %v", llm.ErrUnsupported, err)` wraps the outer sentinel with `%w` but renders the inner `parseSize` error with `%v`, so the parse error is not unwrappable.** Confirmed: `parseSize` returns `fmt.Errorf("invalid size %q (want WxH)", size)` errors, and `Generate` formats them via `%v`. Material impact is negligible (the inner error is a formatted string with no further wrapping), but for consistency the inner error should use `%w` as well. Minor. </details> <details><summary><b>🧹 Code cleanliness & maintainability</b> — Minor issues</summary> All four findings are verifiable against the actual code. Let me confirm each: **Finding 1** (image.go:90-107): Confirmed. Old code returned an explicit `*llm.APIError` per-image with message "image %d returned no inline b64_json data" and a comment explaining the rationale. New code (lines 90-92) silently `continue`s on empty string, only reporting generic "image response contained no images" (line 104) if *all* empty. The "Why error rather than skip" comment is dropped with no replacement. **Finding 2** (image.go:117): Confirmed. `parseSize` calls `strings.ToLower(size)` before splitting on `"x"`, enabling `"512X512"`. Doc comment (line 110-111) says only `"WxH"` — case-insensitive separator not documented. The ToLower only matters for uppercase separator `X`. **Finding 3** (ADR lines 33-35): Confirmed. "First implementation" bullet still states OpenAI `/v1/images/generations` with `response_format: "b64_json"`, now historically inaccurate given the appended Update section (lines 56-63). Not marked superseded. **Finding 4** (image.go:49): Confirmed. Struct comment (lines 31-38) describes pointers/omitempty as the unset-default mechanism. `BatchCount int` (line 49) with `omitempty` uses zero-value semantics (0 = backend default) differently from pointer fields, but this isn't called out. `imagegen.Request.N` (imagegen.go:35-36) confirms `N == 0` = provider default. All findings survive verification. All four findings are cleanliness/maintainability nits (explicitly the reviewer's lens), not correctness blockers. The verdict "Minor issues" is appropriate. ## Corrected review VERDICT: Minor issues - `provider/llamaswap/image.go:90-107` — The old code surfaced an explicit `*llm.APIError` for a `b64_json`-less entry with a helpful "image %d returned no inline b64_json data" message; the new code silently `continue`s on an empty string and only later reports a generic "image response contained no images" (line 104) if *all* entries were empty. The comment explaining the prior "Why error rather than skip" rationale was dropped with no replacement. From a cleanliness standpoint this is a regression in self-documentation: a reader can no longer tell why empty entries are tolerated vs. flagged. Consider either restoring a brief comment (e.g. `// A1111 may return empty slots; skip them, but error if none decode`) or keeping the per-image error. Verified by reading the full `Generate` body (`image.go:90-107`). - `provider/llamaswap/image.go:117` — `parseSize` calls `strings.ToLower(size)` before splitting on `"x"`, which is only meaningful if the separator could be uppercase `"X"` (e.g. `"512X512"`); `strconv.Atoi` handles digits/sign case-insensitively already. That's a real, harmless input-tolerance nicety, but it's undocumented in the function's doc comment (`image.go:110-111`), which says only `"WxH"`. Minor: either mention `"WxH"` is case-insensitive on the separator, or drop the `ToLower` if you don't intend to accept `"X"`. - `docs/adr/0016-imagegen-interface.md:33-35` — The ADR's original "First implementation" bullet still states the provider targets OpenAI `/v1/images/generations` with `response_format: "b64_json"`, which is now historically inaccurate. The new "Update — A1111 txt2img endpoint" section is appended below (lines 56-63), leaving a contradictory in-place claim. For maintainability the stale bullet should be marked superseded (e.g. `~~First implementation: OpenAI /v1/images/generations~~ (superseded; see Update below)`), consistent with how the doc already layers "Update" sections. - `provider/llamaswap/image.go:49` — `BatchCount int` uses `omitempty`, and `req.N == 0` (the documented "provider default" sentinel per `imagegen.Request.N`, `imagegen.go:35-36`) correctly maps to `batch_count` being omitted. However the struct comment (lines 31-38) describes *pointer/omitempty* fields as the unset-default mechanism and then lists `BatchCount` as a plain `int` with `omitempty` without calling out that its zero-value semantics differ (0 = backend default, not "unset"). Minor readability nit: a one-line note that `N`/`batch_count` uses zero-as-default rather than pointer-as-default would keep the comment honest. </details> <details><summary><b>⚡ Performance</b> — No material issues found</summary> **VERDICT: No material issues found** Reviewed through the performance lens. The change is a wire-format swap (OpenAI → A1111), not an algorithmic change: - `parseSize` (image.go:112) runs once per `Generate` call and does a single `SplitN` + two `Atoi` — negligible, not on a hot loop. - Base64 decode loop (image.go:90-98) is unchanged in shape: it still iterates over the response images and decodes each once. No new allocations or quadratic behavior introduced. - `BatchCount: req.N` (image.go:81) maps the request count 1:1 to `batch_count`; the server generates N images in one request rather than the client issuing N requests, so this is if anything more efficient than a loop. - No new unbounded growth, blocking calls, or pagination concerns. Nothing material to flag in the performance lane. </details> <details><summary><b>🧯 Error handling & edge cases</b> — Minor issues</summary> VERDICT: Minor issues - `provider/llamaswap/image.go:62-64,81` — The guard rejects `req.N < 0` but permits `req.N == 0`. With the new A1111 wire shape, `BatchCount` is a plain `int` with `omitempty` (line 49), so `N == 0` maps to `BatchCount: 0` (line 81) and the field is omitted from the request body entirely. The `imagegen.Request` contract (`imagegen/imagegen.go:29,35`) explicitly promises "N == 0 yields the backend's default count (usually one)". Whether sd-server's `/sdapi/v1/txt2img` treats an omitted `batch_count` as 1 (default count) or 0 is not determinable from this repo; if it defaults to 0, the code's own safety net at line 100 (`len(out.Images) == 0` → `"image response contained no images"` APIError) turns a legitimate "default count" request into a hard error. Suggested fix: map `req.N == 0` to `BatchCount: 1`, or document that callers must set `N` explicitly for this provider. (Backend default unverified — no sd-server source in repo.) - `provider/llamaswap/image.go:90-93` — A returned-but-empty image slot (b64 `""`) is now silently `continue`d. The previous code explicitly returned a hard `APIError` ("image %d returned no inline b64_json data") for an empty entry, with a comment explaining the choice. If sd-server returns a mix of populated and empty strings (e.g. a partial mid-batch failure that yields an empty element), the caller now gets a silently-shortened result with no indication that image `i` was dropped; an error only surfaces if the whole batch collapses to empty (line 100). This is a regression in partial-failure visibility. Suggested fix: count skipped entries and surface that in the result/error, or treat a non-empty `resp.Images` array containing any empty string as a partial-failure error as before. - `provider/llamaswap/image.go:66-69` — `parseSize` failure is wrapped with `llm.ErrUnsupported`. A malformed `Size` is a caller-input bug rather than an unsupported-capability condition, so the error category is semantically off; this is consistent with the file's existing use of `ErrUnsupported` for the `N` and prompt guards, so the mismatch is preserved rather than introduced. Note only; no action required. </details> <sub>Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 3m 10s</sub>

🪰 Gadfly review — devstral-2:123b-cloud (ollama-cloud)

Reviewing… this comment will update with findings and run time.

<!-- gadfly-review:ollama:devstral-2:123b-cloud --> ### 🪰 Gadfly review — `devstral-2:123b-cloud` (ollama-cloud) ⏳ Reviewing… this comment will update with findings and run time.

🪰 Gadfly review — deepseek-v4-pro:cloud (ollama-cloud)

Verdict: Minor issues — 5 reviewers: security, correctness, maintainability, performance, error-handling

🔒 Security — No material issues found

No material issues found

I reviewed the full diff and the surrounding code (provider/llamaswap/image.go, provider/llamaswap/llamaswap.go, imagegen/imagegen.go, and the test file) specifically through the security lens. Here's what I checked:

  • Injection: All user-controlled fields (Prompt, NegativePrompt, SizeWidth/Height, Seed, Steps, CFGScale, Sampler, NBatchCount) are serialized via json.Marshal in doJSON, which prevents JSON injection. The backend (sd-server) is the same trusted service as before; no new interpreter boundary is introduced.

  • Path traversal / SSRF: The endpoint path is hardcoded (/sdapi/v1/txt2img). The model field is sent in the JSON body, not interpolated into the URL. doJSON constructs the URL as p.baseURL + path with no user-controlled segments. No SSRF or path-traversal vector.

  • parseSize: The new size-parsing function uses strconv.Atoi (safe against overflow — it errors on out-of-range), validates positivity, and handles whitespace. No injection or overflow risk.

  • BatchCount with omitempty: When req.N == 0, the zero-value int is omitted, deferring to the backend default. This is the documented contract. No abuse vector.

  • Response handling: The txt2imgResponse only carries base64 image strings. The Raw escape hatch exposes no secrets. Empty image entries are now silently skipped (previously errored), but the "no images" sentinel error still fires if all are empty — no information leak.

  • Auth: Unchanged. doJSON still sends Authorization: Bearer <token> (or Bearer no-key placeholder) exactly as before.

  • Model ID validation: The model ID is passed through verbatim (same as before), but it's only used in the JSON body for routing and in error messages — never in a URL path or shell command. The existing Unload path-traversal guard for model IDs is unrelated to this code path.

No material security issues introduced by this change.

🎯 Correctness — No material issues found

Verdict: No material issues found

All three findings in the draft are speculative claims about the field names and semantics expected by an external server (stable-diffusion.cpp's sd-server) that is not part of this repository. The code is internally consistent — sample_method, batch_count, and the req.N → BatchCount mapping are used uniformly across the struct definition, the request construction, the ADR documentation, and the unit tests. Whether the external sd-server build actually honors those exact field names is an integration concern that cannot be confirmed or refuted from the code in this repo alone. None of the findings can be positively verified, so all are dropped.

🧹 Code cleanliness & maintainability — Minor issues

All three findings are confirmed against the actual code. Here is the corrected review:

Verdict: Minor issues

  • imagegen/imagegen.go:14 — stale package doc comment. The package doc still says the first implementation "targets the OpenAI /v1/images/generations endpoint." This PR switches to A1111 /sdapi/v1/txt2img, so the comment is now factually wrong. A future reader who starts at the package doc will be misled.

  • provider/llamaswap/image.go:90-93 — silent skip of empty images replaces previous hard error. The old code returned an *llm.APIError when a response entry had no b64_json data. The new code silently continues past empty strings in resp.Images. If the A1111 endpoint can return empty entries for partially-failed batches, the skip may be intentional — but there's no comment explaining the change in error-handling philosophy. (Note: if all images are empty, lines 100-106 still return an llm.APIError, so the silent skip only applies to partial-empty responses.)

  • provider/llamaswap/image.go:49BatchCount uses plain int with omitempty while every other optional numeric field in the same struct uses a pointer. Steps, CFGScale, Width, Height, and Seed are all *int/*float64/*int64 so that omitempty cleanly distinguishes "caller didn't set" from "caller set zero." BatchCount is plain int, relying on the fact that 0 happens to mean "use default." This works today but is inconsistent with the established pattern in the same struct. If a future model ever treats batch_count=0 as a meaningful value distinct from "unset," the plain-int approach would break silently.

Performance — No material issues found

No material issues found.

The diff makes a single HTTP call per Generate invocation (unchanged), adds one trivial parseSize helper that runs once per request (two heap-allocated *int pointers — negligible against the seconds-to-minutes latency of image generation), and the response iteration remains O(N) over a user-bounded image count. No N+1 queries, no allocations inside hot loops, no unbounded growth, no quadratic behavior introduced. The maxResponseBytes = 64 << 20 limit on the response body is unchanged and remains generous for the base64-encoded payloads the A1111 endpoint returns.

🧯 Error handling & edge cases — No material issues found

No material issues found

The error-handling and edge-case coverage in this diff is solid:

  • parseSize (line 112–127) correctly handles empty string → (nil, nil, nil), malformed formats, zero/negative dimensions, and Atoi overflow — all verified by reading the implementation. The strings.ToLower + SplitN approach correctly handles mixed-case like "512X512".

  • req.N < 0 guard (line 62–64) is preserved. N=0 maps to BatchCount=0 which omitempty drops, letting the backend use its default — consistent with the imagegen.Request contract.

  • Nil-pointer omitempty fields (Seed, Steps, CFGScale, Width, Height) all correctly fall through to backend defaults when unset. Verified by reading both the struct tags and the imagegen.Request zero-value semantics.

  • Empty resp.Images slice (nil or []) is a no-op in for range, falling through to the len(out.Images) == 0APIError guard (line 100–106). Verified.

  • Base64 decode failure (line 94–97) returns a wrapped error — not swallowed. Verified.

  • continue on empty b64 string (line 91–93): The old code errored on an empty b64_json entry; the new code skips empty strings and only errors if all images are empty. This is a deliberate softening — partial results are better than none, and the A1111 images array shouldn't contain empties in normal operation. The "all empty" case is still caught. Reasonable.

  • doJSON (in llamaswap.go:188–227) handles non-2xx via apiError, drains the body on nil out, and bounds reads with io.LimitReader — all pre-existing and unchanged, but confirmed the new call site at line 85 benefits from the same protections.

No panics on bad input, no swallowed errors, no missing cleanup, no nil-pointer dereference paths introduced by this change.

Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 6m 51s

<!-- gadfly-review:ollama:deepseek-v4-pro:cloud --> ### 🪰 Gadfly review — `deepseek-v4-pro:cloud` (ollama-cloud) **Verdict: Minor issues** — 5 reviewers: security, correctness, maintainability, performance, error-handling <details><summary><b>🔒 Security</b> — No material issues found</summary> No material issues found I reviewed the full diff and the surrounding code (`provider/llamaswap/image.go`, `provider/llamaswap/llamaswap.go`, `imagegen/imagegen.go`, and the test file) specifically through the security lens. Here's what I checked: - **Injection**: All user-controlled fields (`Prompt`, `NegativePrompt`, `Size`→`Width`/`Height`, `Seed`, `Steps`, `CFGScale`, `Sampler`, `N`→`BatchCount`) are serialized via `json.Marshal` in `doJSON`, which prevents JSON injection. The backend (sd-server) is the same trusted service as before; no new interpreter boundary is introduced. - **Path traversal / SSRF**: The endpoint path is hardcoded (`/sdapi/v1/txt2img`). The `model` field is sent in the JSON body, not interpolated into the URL. `doJSON` constructs the URL as `p.baseURL + path` with no user-controlled segments. No SSRF or path-traversal vector. - **`parseSize`**: The new size-parsing function uses `strconv.Atoi` (safe against overflow — it errors on out-of-range), validates positivity, and handles whitespace. No injection or overflow risk. - **`BatchCount` with `omitempty`**: When `req.N == 0`, the zero-value `int` is omitted, deferring to the backend default. This is the documented contract. No abuse vector. - **Response handling**: The `txt2imgResponse` only carries base64 image strings. The `Raw` escape hatch exposes no secrets. Empty image entries are now silently skipped (previously errored), but the "no images" sentinel error still fires if all are empty — no information leak. - **Auth**: Unchanged. `doJSON` still sends `Authorization: Bearer <token>` (or `Bearer no-key` placeholder) exactly as before. - **Model ID validation**: The model ID is passed through verbatim (same as before), but it's only used in the JSON body for routing and in error messages — never in a URL path or shell command. The existing `Unload` path-traversal guard for model IDs is unrelated to this code path. No material security issues introduced by this change. </details> <details><summary><b>🎯 Correctness</b> — No material issues found</summary> ## Verdict: No material issues found All three findings in the draft are speculative claims about the field names and semantics expected by an **external server** (stable-diffusion.cpp's sd-server) that is not part of this repository. The code is internally consistent — `sample_method`, `batch_count`, and the `req.N → BatchCount` mapping are used uniformly across the struct definition, the request construction, the ADR documentation, and the unit tests. Whether the external sd-server build actually honors those exact field names is an integration concern that cannot be confirmed or refuted from the code in this repo alone. None of the findings can be positively verified, so all are dropped. </details> <details><summary><b>🧹 Code cleanliness & maintainability</b> — Minor issues</summary> All three findings are confirmed against the actual code. Here is the corrected review: ## Verdict: Minor issues - **`imagegen/imagegen.go:14` — stale package doc comment.** The package doc still says the first implementation "targets the OpenAI `/v1/images/generations` endpoint." This PR switches to A1111 `/sdapi/v1/txt2img`, so the comment is now factually wrong. A future reader who starts at the package doc will be misled. - **`provider/llamaswap/image.go:90-93` — silent skip of empty images replaces previous hard error.** The old code returned an `*llm.APIError` when a response entry had no `b64_json` data. The new code silently `continue`s past empty strings in `resp.Images`. If the A1111 endpoint can return empty entries for partially-failed batches, the skip may be intentional — but there's no comment explaining the change in error-handling philosophy. (Note: if *all* images are empty, lines 100-106 still return an `llm.APIError`, so the silent skip only applies to partial-empty responses.) - **`provider/llamaswap/image.go:49` — `BatchCount` uses plain `int` with `omitempty` while every other optional numeric field in the same struct uses a pointer.** `Steps`, `CFGScale`, `Width`, `Height`, and `Seed` are all `*int`/`*float64`/`*int64` so that `omitempty` cleanly distinguishes "caller didn't set" from "caller set zero." `BatchCount` is plain `int`, relying on the fact that 0 happens to mean "use default." This works today but is inconsistent with the established pattern in the same struct. If a future model ever treats `batch_count=0` as a meaningful value distinct from "unset," the plain-int approach would break silently. </details> <details><summary><b>⚡ Performance</b> — No material issues found</summary> **No material issues found.** The diff makes a single HTTP call per `Generate` invocation (unchanged), adds one trivial `parseSize` helper that runs once per request (two heap-allocated `*int` pointers — negligible against the seconds-to-minutes latency of image generation), and the response iteration remains O(N) over a user-bounded image count. No N+1 queries, no allocations inside hot loops, no unbounded growth, no quadratic behavior introduced. The `maxResponseBytes = 64 << 20` limit on the response body is unchanged and remains generous for the base64-encoded payloads the A1111 endpoint returns. </details> <details><summary><b>🧯 Error handling & edge cases</b> — No material issues found</summary> No material issues found The error-handling and edge-case coverage in this diff is solid: - **`parseSize`** (line 112–127) correctly handles empty string → `(nil, nil, nil)`, malformed formats, zero/negative dimensions, and `Atoi` overflow — all verified by reading the implementation. The `strings.ToLower` + `SplitN` approach correctly handles mixed-case like `"512X512"`. - **`req.N < 0` guard** (line 62–64) is preserved. `N=0` maps to `BatchCount=0` which `omitempty` drops, letting the backend use its default — consistent with the `imagegen.Request` contract. - **Nil-pointer `omitempty` fields** (`Seed`, `Steps`, `CFGScale`, `Width`, `Height`) all correctly fall through to backend defaults when unset. Verified by reading both the struct tags and the `imagegen.Request` zero-value semantics. - **Empty `resp.Images` slice** (nil or `[]`) is a no-op in `for range`, falling through to the `len(out.Images) == 0` → `APIError` guard (line 100–106). Verified. - **Base64 decode failure** (line 94–97) returns a wrapped error — not swallowed. Verified. - **`continue` on empty b64 string** (line 91–93): The old code errored on an empty `b64_json` entry; the new code skips empty strings and only errors if *all* images are empty. This is a deliberate softening — partial results are better than none, and the A1111 `images` array shouldn't contain empties in normal operation. The "all empty" case is still caught. Reasonable. - **`doJSON`** (in `llamaswap.go:188–227`) handles non-2xx via `apiError`, drains the body on nil `out`, and bounds reads with `io.LimitReader` — all pre-existing and unchanged, but confirmed the new call site at line 85 benefits from the same protections. No panics on bad input, no swallowed errors, no missing cleanup, no nil-pointer dereference paths introduced by this change. </details> <sub>Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 6m 51s</sub>

🪰 Gadfly review — claude-code/opus:max (claude-code)

Verdict: Minor issues — 5 reviewers: security, correctness, maintainability, performance, error-handling

🔒 Security — No material issues found

VERDICT: No material issues found

Reviewed through the 🔒 Security lens. I traced the untrusted/attacker-influenceable inputs (model id, prompt, size, and the upstream response body) to where they're used.

  • Request path is a hard-coded constant, not derived from input. image.go:85 POSTs to the literal "/sdapi/v1/txt2img"; the model id is carried in the JSON body (txt2imgRequest.Model, image.go:72), not interpolated into the URL or path. So this change introduces no path/route injection. (Verified by reading doJSON at llamaswap.go:188-227: it builds p.baseURL+path from the fixed constant, and JSON-marshals the body so the model string is escaped — contrast the deliberate /?# rejection on the path-segment Unload route at llamaswap.go:173, which doesn't apply here.)
  • No SSRF introduced — actually reduced. The old imageResponse carried a url field and the code special-cased url-only entries; the new txt2imgResponse is Images []string (base64 only) and the code never fetches a remote URL. Verified at image.go:90-99.
  • Untrusted response is size-bounded. The base64 blobs are decoded into memory (image.go:94), but the response read is capped at maxResponseBytes = 64<<20 via io.LimitReader in doJSON (llamaswap.go:219), so a malicious/runaway upstream can't drive unbounded allocation through this path. Decode errors are surfaced, not panicked (image.go:95-96).
  • Input validation on Size is sound. parseSize (image.go:112-127) rejects malformed input and non-positive dimensions, and strconv.Atoi rejects out-of-range integers — no negative/overflow values reach the wire. (Very large but valid dimensions could be a resource concern on the upstream backend, but that's a DoS/correctness matter on infra outside this library's trust boundary, not a security defect here.)
  • No secret/credential change. The Authorization: Bearer token handling in doJSON (llamaswap.go:207-209) is untouched by this diff; no new logging of body, token, or response.

Nothing in the security lane is materially wrong with this change.

🎯 Correctness — Minor issues

I've verified the draft against the actual code. The mechanical refactor claims all check out. The one substantive "Minor" finding rests on a premise the draft itself admits is unverifiable from the repo (whether sd-server honors batch_count/sample_method) — that's speculation about an external system, not a defect confirmable against the code, so I drop it. The only confirmable residue is the test-coverage gap.

VERDICT: Minor issues

Verified correct (no action):

  • Type rename imageRequest/imageResponsetxt2imgRequest/txt2imgResponse (image.go:39,52) has no dangling references; grep for the old names, /v1/images, and b64_json turns up only the unrelated openai provider and doc comments. Compiles.
  • All req.* fields forwarded (Size, N, Steps, CFGScale, Sampler, Seed, NegativePrompt) exist on imagegen.Request with matching types (Seed *int64, Steps *int, CFGScale *float64); pointer/omitempty fall-through is preserved and asserted by TestImageGenerateSettings (llamaswap_test.go:212-240).
  • parseSize (image.go:112) is correct: ""(nil,nil,nil); rejects non-WxH and non-positive dims; lowercases so 512X512 works; SplitN(...,2) rejects "1x2x3" via the Atoi failure.
  • Empty-image handling is preserved, not silently dropped: the post-loop guard at image.go:100-106 returns an APIError when zero images decode. N==0BatchCount omitted → server default.

Minor — batch path (req.Nbatch_count) is untested:

  • image.go:49,81: BatchCount int \json:"batch_count,omitempty"`req.Nis the new batch mechanism, but no test exercises N>1 or assertsgotBody["batch_count"]TestImageGenerate/TestImageGenerateSettingsonly checkwidth/height/steps/cfg_scale/negative_prompt/sample_method/seed. Adding a test that sends N≥2 and asserts batch_countwould lock the field in. (The draft's stronger claim — that an unrecognizedbatch_count/sample_method` would silently collapse N>1 — depends on sd-server's actual A1111 field names, which cannot be confirmed from this repo; it's a live-build question, not a code defect.)
🧹 Code cleanliness & maintainability — Minor issues

Both findings verified against the source. Finding 1 (stale package doc at imagegen/imagegen.go:13-14) is confirmed — the doc still pins the OpenAI /v1/images/generations endpoint that this PR replaces.

Finding 2 (the "pointers" nit) does not survive: the comment at image.go:37-38 literally reads "Optional fields are pointers/omitempty" — the slash already accounts for the non-pointer omitempty fields (SampleMethod, BatchCount), so the comment is not actually misleading. The "outside my lens" batch_count note is speculative ("worth a correctness check"), not a confirmed finding, so it is dropped as well.


VERDICT: Minor issues

Findings

  • Stale package doc — wrong endpoint imagegen/imagegen.go:13-14. The package doc still says "The first implementation is provider/llamaswap, which targets the OpenAI /v1/images/generations endpoint routed to a stable-diffusion.cpp backend" — exactly the behavior this PR changes. This is a live doc comment (not the append-only ADR), and provider/llamaswap/image.go:56 now POSTs /sdapi/v1/txt2img. CLAUDE.md requires docs to be updated in the same commit as the behavior they describe. Fix: update the sentence to reference the A1111 /sdapi/v1/txt2img endpoint, or generalize it so it doesn't pin a specific endpoint.
Performance — No material issues found

This is a clean, isolated image-generation path. Image generation is dominated by upstream model inference (seconds per render); the Go-side request marshaling and response decoding here are not a hot path. I checked for the usual performance regressions and found nothing material.

VERDICT: No material issues found

  • provider/llamaswap/image.go:90-99 — the response loop over resp.Images does one base64 decode + MIME sniff per returned image. This is O(images) with no nested work, no per-image network round-trip (the bytes are inline, same as before — the old code's url-only branch that could have implied a second fetch was never actually fetching). No N+1 introduced; if anything BatchCount: req.N lets sd-server produce N images in a single request rather than N calls. Verified by reading the full function.
  • provider/llamaswap/image.go:66,112-127parseSize runs once per Generate call, does a couple of small string ops and two Atois. Negligible, not in any loop.
  • No unbounded growth, no quadratic behavior, no blocking calls added on a hot path. The struct field reshuffle and endpoint swap have no allocation impact beyond the pre-existing pattern.

Trivial (not worth acting on): out.Images at line 98 grows via append without preallocation — make([]llm.ImagePart, 0, len(resp.Images)) would avoid a couple of slice regrowths, but for the handful of images a batch returns this is immeasurable against inference cost. Flagging only for completeness; not a finding.

🧯 Error handling & edge cases — No material issues found

I verified against the actual checked-out file (provider/llamaswap/image.go), which is important here — the diff in the task message is truncated: it ends the loop at return out, nil, but the real code has an added guard at lines 100–106 that the diff doesn't show.

Let me trace the unhappy paths through my lens.

Empty / all-empty image array — From the diff alone this looks like a swallowed-error bug: the new loop does if b64 == "" { continue }, so an {"images":[""]} or {"images":[]} response would seem to fall through to a "successful" Result with zero images. But the real file guards it:

if len(out.Images) == 0 {
    return nil, &llm.APIError{ ... Message: "image response contained no images" }
}

So an all-empty/empty response surfaces an error rather than a hollow success. Handled correctly (and consistent with the old per-entry error, just more tolerant of partial empties). Not a finding.

parseSize edge cases — I checked the boundaries:

  • ""(nil,nil,nil) → falls back to model default. ✓
  • "x512", "512x", "512x512x3"Atoi error or len(parts)!=2 → error. ✓
  • negative/zero ("-512x512", "0x0") → w<=0 || h<=0 → error. ✓
  • overflow ("99999999999999999999x512") → Atoi returns error, caught by err1/err2. ✓
  • case ("512X512") → ToLower normalizes. ✓

Robust; the error is wrapped with llm.ErrUnsupported and returned before any network call.

N / BatchCount boundaryreq.N < 0 is rejected; N==0BatchCount==0omitempty drops it → sd-server default. No panic or negative-count path. ✓

Decode failure mid-batchbase64.StdEncoding.DecodeString error returns immediately with a wrapped %w error rather than being swallowed. ✓


VERDICT: No material issues found

Through the error-handling & edge-cases lens, the change is clean. The one path that looked suspect from the diff (silently returning zero images on an empty/all-empty response) is actually closed by the len(out.Images) == 0 guard in the checked-out code — verified by reading provider/llamaswap/image.go:90–107. parseSize rejects every malformed/non-positive/overflowing input before issuing a request, the negative-N guard is intact, and decode errors are propagated rather than ignored.

Outside my lens (one note, not pursued): A1111's /sdapi/v1/txt2img conventionally uses batch_size/n_iter, not batch_count — if sd-server ignores the unrecognized field, N>1 would silently yield one image. That's a correctness/API-contract question for another lens, not an error-handling defect.

Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 2m 43s

<!-- gadfly-review:ollama:claude-code/opus:max --> ### 🪰 Gadfly review — `claude-code/opus:max` (claude-code) **Verdict: Minor issues** — 5 reviewers: security, correctness, maintainability, performance, error-handling <details><summary><b>🔒 Security</b> — No material issues found</summary> ## VERDICT: No material issues found Reviewed through the **🔒 Security** lens. I traced the untrusted/attacker-influenceable inputs (model id, prompt, size, and the upstream response body) to where they're used. - **Request path is a hard-coded constant, not derived from input.** `image.go:85` POSTs to the literal `"/sdapi/v1/txt2img"`; the model id is carried in the JSON *body* (`txt2imgRequest.Model`, `image.go:72`), not interpolated into the URL or path. So this change introduces no path/route injection. (Verified by reading `doJSON` at `llamaswap.go:188-227`: it builds `p.baseURL+path` from the fixed constant, and JSON-marshals the body so the model string is escaped — contrast the deliberate `/?#` rejection on the path-segment `Unload` route at `llamaswap.go:173`, which doesn't apply here.) - **No SSRF introduced — actually reduced.** The old `imageResponse` carried a `url` field and the code special-cased url-only entries; the new `txt2imgResponse` is `Images []string` (base64 only) and the code never fetches a remote URL. Verified at `image.go:90-99`. - **Untrusted response is size-bounded.** The base64 blobs are decoded into memory (`image.go:94`), but the response read is capped at `maxResponseBytes = 64<<20` via `io.LimitReader` in `doJSON` (`llamaswap.go:219`), so a malicious/runaway upstream can't drive unbounded allocation through this path. Decode errors are surfaced, not panicked (`image.go:95-96`). - **Input validation on `Size` is sound.** `parseSize` (`image.go:112-127`) rejects malformed input and non-positive dimensions, and `strconv.Atoi` rejects out-of-range integers — no negative/overflow values reach the wire. (Very large but valid dimensions could be a resource concern on the *upstream* backend, but that's a DoS/correctness matter on infra outside this library's trust boundary, not a security defect here.) - **No secret/credential change.** The `Authorization: Bearer` token handling in `doJSON` (`llamaswap.go:207-209`) is untouched by this diff; no new logging of body, token, or response. Nothing in the security lane is materially wrong with this change. </details> <details><summary><b>🎯 Correctness</b> — Minor issues</summary> I've verified the draft against the actual code. The mechanical refactor claims all check out. The one substantive "Minor" finding rests on a premise the draft itself admits is unverifiable from the repo (whether sd-server honors `batch_count`/`sample_method`) — that's speculation about an external system, not a defect confirmable against the code, so I drop it. The only confirmable residue is the test-coverage gap. ## VERDICT: Minor issues **Verified correct (no action):** - Type rename `imageRequest`/`imageResponse` → `txt2imgRequest`/`txt2imgResponse` (`image.go:39,52`) has no dangling references; grep for the old names, `/v1/images`, and `b64_json` turns up only the unrelated openai provider and doc comments. Compiles. - All `req.*` fields forwarded (`Size`, `N`, `Steps`, `CFGScale`, `Sampler`, `Seed`, `NegativePrompt`) exist on `imagegen.Request` with matching types (`Seed *int64`, `Steps *int`, `CFGScale *float64`); pointer/`omitempty` fall-through is preserved and asserted by `TestImageGenerateSettings` (`llamaswap_test.go:212-240`). - `parseSize` (`image.go:112`) is correct: `""`→`(nil,nil,nil)`; rejects non-`WxH` and non-positive dims; lowercases so `512X512` works; `SplitN(...,2)` rejects `"1x2x3"` via the `Atoi` failure. - Empty-image handling is preserved, not silently dropped: the post-loop guard at `image.go:100-106` returns an `APIError` when zero images decode. `N==0` → `BatchCount` omitted → server default. **Minor — batch path (`req.N` → `batch_count`) is untested:** - `image.go:49,81`: `BatchCount int \`json:"batch_count,omitempty"\`` ← `req.N` is the new batch mechanism, but no test exercises N>1 or asserts `gotBody["batch_count"]` — `TestImageGenerate`/`TestImageGenerateSettings` only check `width/height/steps/cfg_scale/negative_prompt/sample_method/seed`. Adding a test that sends N≥2 and asserts `batch_count` would lock the field in. (The draft's stronger claim — that an unrecognized `batch_count`/`sample_method` would silently collapse N>1 — depends on sd-server's actual A1111 field names, which cannot be confirmed from this repo; it's a live-build question, not a code defect.) </details> <details><summary><b>🧹 Code cleanliness & maintainability</b> — Minor issues</summary> Both findings verified against the source. Finding 1 (stale package doc at `imagegen/imagegen.go:13-14`) is confirmed — the doc still pins the OpenAI `/v1/images/generations` endpoint that this PR replaces. Finding 2 (the "pointers" nit) does not survive: the comment at `image.go:37-38` literally reads "Optional fields are **pointers/omitempty**" — the slash already accounts for the non-pointer `omitempty` fields (`SampleMethod`, `BatchCount`), so the comment is not actually misleading. The "outside my lens" `batch_count` note is speculative ("worth a correctness check"), not a confirmed finding, so it is dropped as well. --- ## VERDICT: Minor issues **Findings** - **Stale package doc — wrong endpoint** `imagegen/imagegen.go:13-14`. The package doc still says *"The first implementation is provider/llamaswap, which targets the OpenAI `/v1/images/generations` endpoint routed to a stable-diffusion.cpp backend"* — exactly the behavior this PR changes. This is a live doc comment (not the append-only ADR), and `provider/llamaswap/image.go:56` now POSTs `/sdapi/v1/txt2img`. CLAUDE.md requires docs to be updated in the same commit as the behavior they describe. Fix: update the sentence to reference the A1111 `/sdapi/v1/txt2img` endpoint, or generalize it so it doesn't pin a specific endpoint. </details> <details><summary><b>⚡ Performance</b> — No material issues found</summary> This is a clean, isolated image-generation path. Image generation is dominated by upstream model inference (seconds per render); the Go-side request marshaling and response decoding here are not a hot path. I checked for the usual performance regressions and found nothing material. **VERDICT: No material issues found** - `provider/llamaswap/image.go:90-99` — the response loop over `resp.Images` does one base64 decode + MIME sniff per returned image. This is O(images) with no nested work, no per-image network round-trip (the bytes are inline, same as before — the old code's url-only branch that *could* have implied a second fetch was never actually fetching). No N+1 introduced; if anything `BatchCount: req.N` lets sd-server produce N images in a single request rather than N calls. Verified by reading the full function. - `provider/llamaswap/image.go:66,112-127` — `parseSize` runs once per `Generate` call, does a couple of small string ops and two `Atoi`s. Negligible, not in any loop. - No unbounded growth, no quadratic behavior, no blocking calls added on a hot path. The struct field reshuffle and endpoint swap have no allocation impact beyond the pre-existing pattern. Trivial (not worth acting on): `out.Images` at line 98 grows via `append` without preallocation — `make([]llm.ImagePart, 0, len(resp.Images))` would avoid a couple of slice regrowths, but for the handful of images a batch returns this is immeasurable against inference cost. Flagging only for completeness; not a finding. </details> <details><summary><b>🧯 Error handling & edge cases</b> — No material issues found</summary> I verified against the actual checked-out file (`provider/llamaswap/image.go`), which is important here — the diff in the task message is **truncated**: it ends the loop at `return out, nil`, but the real code has an added guard at lines 100–106 that the diff doesn't show. Let me trace the unhappy paths through my lens. **Empty / all-empty image array** — From the diff alone this looks like a swallowed-error bug: the new loop does `if b64 == "" { continue }`, so an `{"images":[""]}` or `{"images":[]}` response would seem to fall through to a "successful" `Result` with zero images. But the real file guards it: ```go if len(out.Images) == 0 { return nil, &llm.APIError{ ... Message: "image response contained no images" } } ``` So an all-empty/empty response surfaces an error rather than a hollow success. Handled correctly (and consistent with the old per-entry error, just more tolerant of partial empties). **Not a finding.** **`parseSize` edge cases** — I checked the boundaries: - `""` → `(nil,nil,nil)` → falls back to model default. ✓ - `"x512"`, `"512x"`, `"512x512x3"` → `Atoi` error or `len(parts)!=2` → error. ✓ - negative/zero (`"-512x512"`, `"0x0"`) → `w<=0 || h<=0` → error. ✓ - overflow (`"99999999999999999999x512"`) → `Atoi` returns error, caught by `err1/err2`. ✓ - case (`"512X512"`) → `ToLower` normalizes. ✓ Robust; the error is wrapped with `llm.ErrUnsupported` and returned before any network call. **`N` / `BatchCount` boundary** — `req.N < 0` is rejected; `N==0` → `BatchCount==0` → `omitempty` drops it → sd-server default. No panic or negative-count path. ✓ **Decode failure mid-batch** — `base64.StdEncoding.DecodeString` error returns immediately with a wrapped `%w` error rather than being swallowed. ✓ --- VERDICT: **No material issues found** Through the error-handling & edge-cases lens, the change is clean. The one path that looked suspect from the diff (silently returning zero images on an empty/all-empty response) is actually closed by the `len(out.Images) == 0` guard in the checked-out code — verified by reading `provider/llamaswap/image.go:90–107`. `parseSize` rejects every malformed/non-positive/overflowing input before issuing a request, the negative-`N` guard is intact, and decode errors are propagated rather than ignored. *Outside my lens (one note, not pursued): A1111's `/sdapi/v1/txt2img` conventionally uses `batch_size`/`n_iter`, not `batch_count` — if sd-server ignores the unrecognized field, `N>1` would silently yield one image. That's a correctness/API-contract question for another lens, not an error-handling defect.* </details> <sub>Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 2m 43s</sub>

🪰 Gadfly review — kimi-k2.6:cloud (ollama-cloud)

Verdict: Minor issues — 5 reviewers: security, correctness, maintainability, performance, error-handling

🔒 Security — No material issues found

No material issues found

Reviewed through the Security lens. The change switches the image generation endpoint from OpenAI /v1/images/generations to A1111 /sdapi/v1/txt2img, refactors the request/response structs, and adds a parseSize helper.

Key security-relevant findings after verification:

  • No injection vectors: parseSize at provider/llamaswap/image.go:112 only extracts integer dimensions from a "WxH" string using strconv.Atoi; the values are emitted as JSON numbers, not concatenated into URLs, commands, or queries. SplitN with limit 2 safely handles extra "x" characters.
  • No SSRF introduced: The endpoint path is hardcoded ("/sdapi/v1/txt2img"); user input (req.Size, req.Prompt, etc.) never influences the request URL.
  • Authn/authz unchanged: doJSON still attaches the Bearer token the same way; nothing in the PR adds, removes, or weakens authentication.
  • Base64 handling is safe: The old code's strict b64_json check existed because the OpenAI endpoint could return either inline data or a remote URL. The A1111 endpoint only returns base64 strings in images; skipping empty entries is correct because there is no URL-fetch fallback to accidentally trigger. base64.StdEncoding.DecodeString and http.DetectContentType on the decoded bytes are standard and safe.
  • No secret leakage: No credentials, tokens, or seeds are logged or exposed in error messages.
  • Input validation is adequate: req.N < 0 is rejected; width/height must be > 0; parseSize returns an error on malformed input.

Nothing in this diff introduces an authn/authz gap, injection risk, unsafe deserialization, secret leakage, or insecure default.

🎯 Correctness — No material issues found

Verdict: No material issues found

After a thorough correctness review:

  • parseSize correctly handles the documented "WxH" format, rejects invalid/negative/zero dimensions, and returns (nil, nil) for an empty string so the backend default applies. Verified by reading the function in provider/llamaswap/image.go:112-127.
  • Field mapping from imagegen.Request to txt2imgRequest preserves the semantic intent: optional overrides remain pointers with omitempty so the backend falls back to its own defaults; Size is properly split into width/height; N maps to batch_count. Verified by reading the struct and Generate in provider/llamaswap/image.go:39-82.
  • Response handling aligns with the A1111 shape (images []string). The loop skips empty strings rather than hard-failing on the first missing entry, but the final len(out.Images) == 0 check still guarantees at least one valid image is returned. Verified in provider/llamaswap/image.go:89-107.
  • Tests exercise the new endpoint path, response shape, and omission behavior for optional fields. The absence of width/height/batch_count from the omission assertion in TestImageGenerateSettings is a test-coverage gap, not a code correctness bug.

No logic errors, incorrect constants, or semantic/domain bugs were found in the change itself.

🧹 Code cleanliness & maintainability — Minor issues

Minor issues

  • provider/llamaswap/image.go:112 — New parseSize has zero unit tests. It has several branches (empty, malformed, non-positive numbers, whitespace trimming) but the test file only exercises the happy path ("512x512") indirectly through TestImageGenerate. Add a small table-driven TestParseSize covering "", "512x512", " 512X512 ", "512", "0x512", "-1x512" to prevent regressions in size parsing.

  • provider/llamaswap/image.go:68 — Error wrapping uses %v for the inner error instead of %w, breaking error-chain inspection:
    fmt.Errorf("%w: %v", llm.ErrUnsupported, err) should be fmt.Errorf("%w: %w", llm.ErrUnsupported, err) (or %w: %s if the inner error should not be wrapped).

  • provider/llamaswap/image.go:91 — Empty base64 entries are silently skipped with continue but there is no comment explaining why. The old code had a detailed comment justifying the hard error; the new skip behavior should have at least a // skip empty entries note so future readers know it's intentional and not a missing validation.

Performance — No material issues found

No material issues found.

🧯 Error handling & edge cases — No material issues found

No material issues found

After walking through the diff with the error-handling lens:

  • parseSize handles its edge cases correctly: empty string yields nil,nil,nil; non-"WxH" shapes, non-numeric components, zero/negative dimensions, and overflow all return errors. TrimSpace + ToLower + SplitN(..., "x", 2) is robust against extra spaces and mixed case.
  • req.N < 0 and empty req.Prompt are validated before the request is built.
  • Zero-value BatchCount is omitted via omitempty, matching the imagegen.Request contract where N == 0 means "backend default".
  • resp.Images fully-empty (nil or all empty strings) is caught by the post-loop len(out.Images) == 0 check, returning an APIError.
  • Malformed base64 in a non-empty entry surfaces via base64.DecodeString with a wrapped error including the index.
  • doJSON propagates HTTP and decode errors and still defers resp.Body.Close().

The switch from erroring on a single empty b64_json entry to continue-ing on an empty string in resp.Images is justified by the endpoint change: the old check existed because the OpenAI endpoint could ignore response_format and return URLs instead; the A1111 endpoint has no such response_format concept, and a total failure is still caught by the zero-images guard.

I found no swallowed errors, missing cleanups, nil dereference risks, or unhandled boundary conditions introduced by this change.

Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 8m 9s

<!-- gadfly-review:ollama:kimi-k2.6:cloud --> ### 🪰 Gadfly review — `kimi-k2.6:cloud` (ollama-cloud) **Verdict: Minor issues** — 5 reviewers: security, correctness, maintainability, performance, error-handling <details><summary><b>🔒 Security</b> — No material issues found</summary> **No material issues found** Reviewed through the **Security** lens. The change switches the image generation endpoint from OpenAI `/v1/images/generations` to A1111 `/sdapi/v1/txt2img`, refactors the request/response structs, and adds a `parseSize` helper. Key security-relevant findings after verification: - **No injection vectors**: `parseSize` at `provider/llamaswap/image.go:112` only extracts integer dimensions from a `"WxH"` string using `strconv.Atoi`; the values are emitted as JSON numbers, not concatenated into URLs, commands, or queries. `SplitN` with limit 2 safely handles extra `"x"` characters. - **No SSRF introduced**: The endpoint path is hardcoded (`"/sdapi/v1/txt2img"`); user input (`req.Size`, `req.Prompt`, etc.) never influences the request URL. - **Authn/authz unchanged**: `doJSON` still attaches the Bearer token the same way; nothing in the PR adds, removes, or weakens authentication. - **Base64 handling is safe**: The old code's strict `b64_json` check existed because the OpenAI endpoint could return either inline data or a remote URL. The A1111 endpoint only returns base64 strings in `images`; skipping empty entries is correct because there is no URL-fetch fallback to accidentally trigger. `base64.StdEncoding.DecodeString` and `http.DetectContentType` on the decoded bytes are standard and safe. - **No secret leakage**: No credentials, tokens, or seeds are logged or exposed in error messages. - **Input validation is adequate**: `req.N < 0` is rejected; width/height must be `> 0`; `parseSize` returns an error on malformed input. Nothing in this diff introduces an authn/authz gap, injection risk, unsafe deserialization, secret leakage, or insecure default. </details> <details><summary><b>🎯 Correctness</b> — No material issues found</summary> **Verdict: No material issues found** After a thorough correctness review: - **`parseSize`** correctly handles the documented `"WxH"` format, rejects invalid/negative/zero dimensions, and returns `(nil, nil)` for an empty string so the backend default applies. Verified by reading the function in `provider/llamaswap/image.go:112-127`. - **Field mapping** from `imagegen.Request` to `txt2imgRequest` preserves the semantic intent: optional overrides remain pointers with `omitempty` so the backend falls back to its own defaults; `Size` is properly split into `width`/`height`; `N` maps to `batch_count`. Verified by reading the struct and `Generate` in `provider/llamaswap/image.go:39-82`. - **Response handling** aligns with the A1111 shape (`images []string`). The loop skips empty strings rather than hard-failing on the first missing entry, but the final `len(out.Images) == 0` check still guarantees at least one valid image is returned. Verified in `provider/llamaswap/image.go:89-107`. - **Tests** exercise the new endpoint path, response shape, and omission behavior for optional fields. The absence of `width`/`height`/`batch_count` from the omission assertion in `TestImageGenerateSettings` is a test-coverage gap, not a code correctness bug. No logic errors, incorrect constants, or semantic/domain bugs were found in the change itself. </details> <details><summary><b>🧹 Code cleanliness & maintainability</b> — Minor issues</summary> **Minor issues** - `provider/llamaswap/image.go:112` — New `parseSize` has **zero unit tests**. It has several branches (empty, malformed, non-positive numbers, whitespace trimming) but the test file only exercises the happy path ("512x512") indirectly through `TestImageGenerate`. Add a small table-driven `TestParseSize` covering `""`, `"512x512"`, `" 512X512 "`, `"512"`, `"0x512"`, `"-1x512"` to prevent regressions in size parsing. - `provider/llamaswap/image.go:68` — Error wrapping uses `%v` for the inner error instead of `%w`, breaking error-chain inspection: `fmt.Errorf("%w: %v", llm.ErrUnsupported, err)` should be `fmt.Errorf("%w: %w", llm.ErrUnsupported, err)` (or `%w: %s` if the inner error should not be wrapped). - `provider/llamaswap/image.go:91` — Empty base64 entries are silently skipped with `continue` but there is no comment explaining *why*. The old code had a detailed comment justifying the hard error; the new skip behavior should have at least a `// skip empty entries` note so future readers know it's intentional and not a missing validation. </details> <details><summary><b>⚡ Performance</b> — No material issues found</summary> No material issues found. </details> <details><summary><b>🧯 Error handling & edge cases</b> — No material issues found</summary> No material issues found After walking through the diff with the error-handling lens: - **`parseSize` handles its edge cases correctly:** empty string yields `nil,nil,nil`; non-`"WxH"` shapes, non-numeric components, zero/negative dimensions, and overflow all return errors. `TrimSpace` + `ToLower` + `SplitN(..., "x", 2)` is robust against extra spaces and mixed case. - **`req.N < 0` and empty `req.Prompt` are validated** before the request is built. - **Zero-value `BatchCount`** is omitted via `omitempty`, matching the `imagegen.Request` contract where `N == 0` means "backend default". - **`resp.Images` fully-empty** (nil or all empty strings) is caught by the post-loop `len(out.Images) == 0` check, returning an `APIError`. - **Malformed base64** in a non-empty entry surfaces via `base64.DecodeString` with a wrapped error including the index. - **`doJSON` propagates HTTP and decode errors** and still `defer`s `resp.Body.Close()`. The switch from erroring on a single empty `b64_json` entry to `continue`-ing on an empty string in `resp.Images` is justified by the endpoint change: the old check existed because the OpenAI endpoint could ignore `response_format` and return URLs instead; the A1111 endpoint has no such `response_format` concept, and a total failure is still caught by the zero-images guard. I found no swallowed errors, missing cleanups, nil dereference risks, or unhandled boundary conditions introduced by this change. </details> <sub>Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 8m 9s</sub>

🪰 Gadfly review — qwen3.5:397b-cloud (ollama-cloud)

Verdict: Minor issues — 5 reviewers: security, correctness, maintainability, performance, error-handling

🔒 Security — Minor issues

Verdict: Minor issues found

Security findings (input validation / DoS vectors):

  1. provider/llamaswap/image.go:123 — No upper bound on width/height in parseSize

    • Impact: A caller can request arbitrarily large dimensions (e.g., "9999999x9999999"). The validation only checks w <= 0 || h <= 0, so any positive integer passes. This could cause backend resource exhaustion (memory/CPU) or trigger costly renders, enabling DoS by authenticated users.
    • Fix: Add reasonable upper bounds (e.g., 4096 or 8192 depending on expected model limits) and reject sizes exceeding them.
  2. provider/llamaswap/image.go:62-64 — No upper bound on batch count N

    • Impact: The check req.N < 0 only prevents negative values. A caller can request N=1000000 images in one batch, which gets forwarded as BatchCount to the backend. This could exhaust backend resources or cause extremely long-running renders.
    • Fix: Add a reasonable maximum (e.g., 16 or 32 images per batch) and reject requests exceeding it.

Both issues are input validation gaps that could be exploited for denial-of-service by authenticated API users. They don't enable remote code execution or data leakage, but they do allow resource exhaustion attacks against the image generation backend.

🎯 Correctness — No material issues found

VERDICT: No material issues found

All three findings in the draft review have been dropped after verification against the actual code:

  1. Finding 1 (image.go:48-49 field names) — DROPPED. The code intentionally uses sample_method and batch_count for the stable-diffusion.cpp sd-server implementation. The ADR document (docs/adr/0016-imagegen-interface.md:52) explicitly documents these field names as the intended contract with sd-server. The draft incorrectly assumed standard A1111 webui field names (sampler_name/batch_size), but this code targets the stable-diffusion.cpp project's sd-server which uses different field names. A repo-wide grep confirms no usage of sampler_name or batch_size anywhere.

  2. Finding 2 (test validates incorrect field) — DROPPED. This finding depends on Finding 1 being correct. Since sample_method is the intentional field name for this sd-server implementation, the test at line 235 correctly validates the expected behavior.

  3. Finding 3 (BatchCount omitempty semantics) — DROPPED. While the code structure (int with omitempty) can be confirmed, the draft itself acknowledges this "may be intentional" and matches the original OpenAI code pattern. Without access to sd-server's documented contract for batch_count=0, this cannot be positively confirmed as a bug rather than intended design (N=0 likely means "use backend default", which aligns with omitting the field).

🧹 Code cleanliness & maintainability — No material issues found

VERDICT: No material issues found

Analysis through the code cleanliness & maintainability lens:

  • No dead code left behind: Verified via grep that old types imageRequest and imageResponse are completely removed from the codebase — no orphaned definitions.

  • Naming and structure are clear: New types txt2imgRequest/txt2imgResponse are well-named and match their endpoint. The parseSize() helper is a focused, single-responsibility function that cleanly splits the "WxH" parsing logic out of Generate().

  • Comments explain the "why": The struct comments (lines 31-38, 52-54) document not just the shape but why this endpoint is used over the OpenAI one — excellent maintainability practice.

  • Consistent optional-field pattern: Pointer types with omitempty for Seed, Steps, CFGScale, Width, Height correctly allow sd-server to use its baked defaults when unset.

  • Test updates match the change: Test assertions updated to check width/height integers instead of size string, and response JSON structure matches the new txt2imgResponse.

  • Verified by reading: Confirmed in provider/llamaswap/image.go that no references to old types remain, and the new code follows the same patterns as surrounding code.

The change is a clean refactor with no maintainability debt introduced.

Performance — No material issues found

VERDICT: No material issues found

Performance lens findings:

After reviewing the change through the performance lens:

  1. No N+1 regression: The code correctly uses BatchCount: req.N (line 81) to request all N images in a single API call, matching the old behavior where N was sent to the OpenAI endpoint.

  2. No new allocations: The response structure changed from []struct{B64JSON string} to []string, which is actually fewer allocations per image returned.

  3. Bounded memory: The doJSON helper uses io.LimitReader with maxResponseBytes (64MB cap) for response decoding, preventing unbounded growth.

  4. No hot-path work added: parseSize is called once per request (not in a loop), and base64 decoding is necessary work that was already performed in the old code.

  5. No blocking calls introduced: The HTTP request pattern is unchanged; still a single POST with bounded response reading.

Verified by: Reading provider/llamaswap/image.go (full file), provider/llamaswap/llamaswap.go:188-227 (doJSON implementation), and confirming maxResponseBytes is defined and used for bounding.

The endpoint switch from OpenAI /v1/images/generations to A1111 /sdapi/v1/txt2img does not introduce performance regressions in the code path reviewed.

🧯 Error handling & edge cases — Minor issues

VERDICT: Minor issues found

  • provider/llamaswap/image.go:91-92 — Empty base64 entries now silently skipped (continue) instead of returning an error. The old code explicitly errored on empty b64_json with the rationale that url-only entries meant the backend ignored response_format and the client doesn't fetch remote content. The new behavior is more lenient: if the A1111 endpoint returns a mix of valid and empty entries, partial failures are masked. Note: there is a check at line 100-106 that errors if all images are empty, but mixed results (some valid, some empty) will silently drop the empty ones. Suggested fix: either restore the error (if the A1111 endpoint should never return empty entries) or add a warning/log when skipping to aid debugging.

  • provider/llamaswap/image.go:110-126parseSize validates format and positive values but has no upper bound on dimensions. Extremely large values (e.g., "999999999x999999999") pass validation and are sent to the upstream, which may reject them or fail unpredictably. Suggested fix: add reasonable max dimension checks (e.g., 4096 or 8192) if the upstream has known limits, or document that validation is minimal.

Both issues are in the error-handling/edge-case lane. The first is a behavioral regression on how partial failures are handled; the second is a missing guard on an input boundary.

Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 4m 6s

<!-- gadfly-review:ollama:qwen3.5:397b-cloud --> ### 🪰 Gadfly review — `qwen3.5:397b-cloud` (ollama-cloud) **Verdict: Minor issues** — 5 reviewers: security, correctness, maintainability, performance, error-handling <details><summary><b>🔒 Security</b> — Minor issues</summary> **Verdict: Minor issues found** **Security findings (input validation / DoS vectors):** 1. **`provider/llamaswap/image.go:123` — No upper bound on width/height in `parseSize`** - **Impact**: A caller can request arbitrarily large dimensions (e.g., `"9999999x9999999"`). The validation only checks `w <= 0 || h <= 0`, so any positive integer passes. This could cause backend resource exhaustion (memory/CPU) or trigger costly renders, enabling DoS by authenticated users. - **Fix**: Add reasonable upper bounds (e.g., 4096 or 8192 depending on expected model limits) and reject sizes exceeding them. 2. **`provider/llamaswap/image.go:62-64` — No upper bound on batch count `N`** - **Impact**: The check `req.N < 0` only prevents negative values. A caller can request `N=1000000` images in one batch, which gets forwarded as `BatchCount` to the backend. This could exhaust backend resources or cause extremely long-running renders. - **Fix**: Add a reasonable maximum (e.g., 16 or 32 images per batch) and reject requests exceeding it. Both issues are input validation gaps that could be exploited for denial-of-service by authenticated API users. They don't enable remote code execution or data leakage, but they do allow resource exhaustion attacks against the image generation backend. </details> <details><summary><b>🎯 Correctness</b> — No material issues found</summary> **VERDICT: No material issues found** All three findings in the draft review have been dropped after verification against the actual code: 1. **Finding 1 (image.go:48-49 field names)** — DROPPED. The code intentionally uses `sample_method` and `batch_count` for the stable-diffusion.cpp sd-server implementation. The ADR document (`docs/adr/0016-imagegen-interface.md:52`) explicitly documents these field names as the intended contract with sd-server. The draft incorrectly assumed standard A1111 webui field names (`sampler_name`/`batch_size`), but this code targets the stable-diffusion.cpp project's sd-server which uses different field names. A repo-wide grep confirms no usage of `sampler_name` or `batch_size` anywhere. 2. **Finding 2 (test validates incorrect field)** — DROPPED. This finding depends on Finding 1 being correct. Since `sample_method` is the intentional field name for this sd-server implementation, the test at line 235 correctly validates the expected behavior. 3. **Finding 3 (BatchCount omitempty semantics)** — DROPPED. While the code structure (`int` with `omitempty`) can be confirmed, the draft itself acknowledges this "may be intentional" and matches the original OpenAI code pattern. Without access to sd-server's documented contract for `batch_count=0`, this cannot be positively confirmed as a bug rather than intended design (N=0 likely means "use backend default", which aligns with omitting the field). </details> <details><summary><b>🧹 Code cleanliness & maintainability</b> — No material issues found</summary> **VERDICT: No material issues found** **Analysis through the code cleanliness & maintainability lens:** - **No dead code left behind**: Verified via grep that old types `imageRequest` and `imageResponse` are completely removed from the codebase — no orphaned definitions. - **Naming and structure are clear**: New types `txt2imgRequest`/`txt2imgResponse` are well-named and match their endpoint. The `parseSize()` helper is a focused, single-responsibility function that cleanly splits the "WxH" parsing logic out of `Generate()`. - **Comments explain the "why"**: The struct comments (lines 31-38, 52-54) document not just the shape but *why* this endpoint is used over the OpenAI one — excellent maintainability practice. - **Consistent optional-field pattern**: Pointer types with `omitempty` for `Seed`, `Steps`, `CFGScale`, `Width`, `Height` correctly allow sd-server to use its baked defaults when unset. - **Test updates match the change**: Test assertions updated to check `width`/`height` integers instead of `size` string, and response JSON structure matches the new `txt2imgResponse`. - **Verified by reading**: Confirmed in `provider/llamaswap/image.go` that no references to old types remain, and the new code follows the same patterns as surrounding code. The change is a clean refactor with no maintainability debt introduced. </details> <details><summary><b>⚡ Performance</b> — No material issues found</summary> **VERDICT: No material issues found** **Performance lens findings:** After reviewing the change through the performance lens: 1. **No N+1 regression**: The code correctly uses `BatchCount: req.N` (line 81) to request all N images in a single API call, matching the old behavior where `N` was sent to the OpenAI endpoint. 2. **No new allocations**: The response structure changed from `[]struct{B64JSON string}` to `[]string`, which is actually *fewer* allocations per image returned. 3. **Bounded memory**: The `doJSON` helper uses `io.LimitReader` with `maxResponseBytes` (64MB cap) for response decoding, preventing unbounded growth. 4. **No hot-path work added**: `parseSize` is called once per request (not in a loop), and base64 decoding is necessary work that was already performed in the old code. 5. **No blocking calls introduced**: The HTTP request pattern is unchanged; still a single POST with bounded response reading. **Verified by**: Reading `provider/llamaswap/image.go` (full file), `provider/llamaswap/llamaswap.go:188-227` (doJSON implementation), and confirming `maxResponseBytes` is defined and used for bounding. The endpoint switch from OpenAI `/v1/images/generations` to A1111 `/sdapi/v1/txt2img` does not introduce performance regressions in the code path reviewed. </details> <details><summary><b>🧯 Error handling & edge cases</b> — Minor issues</summary> **VERDICT: Minor issues found** - **`provider/llamaswap/image.go:91-92`** — Empty base64 entries now silently skipped (`continue`) instead of returning an error. The old code explicitly errored on empty `b64_json` with the rationale that url-only entries meant the backend ignored `response_format` and the client doesn't fetch remote content. The new behavior is more lenient: if the A1111 endpoint returns a mix of valid and empty entries, partial failures are masked. Note: there is a check at line 100-106 that errors if *all* images are empty, but mixed results (some valid, some empty) will silently drop the empty ones. Suggested fix: either restore the error (if the A1111 endpoint should never return empty entries) or add a warning/log when skipping to aid debugging. - **`provider/llamaswap/image.go:110-126`** — `parseSize` validates format and positive values but has no upper bound on dimensions. Extremely large values (e.g., `"999999999x999999999"`) pass validation and are sent to the upstream, which may reject them or fail unpredictably. Suggested fix: add reasonable max dimension checks (e.g., 4096 or 8192) if the upstream has known limits, or document that validation is minimal. Both issues are in the error-handling/edge-case lane. The first is a behavioral regression on how partial failures are handled; the second is a missing guard on an input boundary. </details> <sub>Automated adversarial review by Gadfly. Advisory only — does not block merge. · ⏱️ reviewed in 4m 6s</sub>
steve merged commit fe44a6da26 into main 2026-06-29 03:09:29 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: steve/majordomo#10