Skip to content

feat(generate-image): add Atlas Cloud as an optional provider - #1618

Merged
waleedkadous merged 3 commits into
cluesmith:mainfrom
binyangzhu000-sudo:feat/atlascloud-image-provider
Sep 4, 2026
Merged

feat(generate-image): add Atlas Cloud as an optional provider#1618
waleedkadous merged 3 commits into
cluesmith:mainfrom
binyangzhu000-sudo:feat/atlascloud-image-provider

Conversation

@binyangzhu000-sudo

Copy link
Copy Markdown
Contributor

Summary

codev generate-image goes through the @google/genai SDK, so it needs a Google AI Studio key.
Atlas Cloud serves the same Nano Banana Pro model over its own submit-then-poll REST API, so
this adds it behind -p/--provider atlas. The default stays gemini and its code path is untouched.

No new dependency — the Atlas path uses global fetch (the package already requires Node >= 20).

Every mapping was measured, not assumed

Existing flag On --provider atlas Measured
-a/--aspect maps to Atlas's aspect_ratio, honoured 1:1 → 1024×1024, 16:9 → 1376×768
-r/--resolution no equivalent field prints a note; the model's default resolution comes back
--ref not supported (text-to-image only) exits with an error pointing at --provider gemini
output file model returns JPEG named after its actual bytes, rename logged

The two behaviours I deliberately did not paper over:

  • --resolution is not silently dropped. A note is printed, because a user asking for 4K and
    getting the default resolution with no explanation is worse than being told.
  • --ref errors instead of generating. Ignoring reference images while still producing an image
    would look like success.
  • The output file is renamed (output.pngoutput.jpg) rather than writing JPEG bytes into a
    .png. The Gemini path is unaffected.

Validation

  • Live end-to-end: generated through Atlas with aspect: '16:9' → a real 685 KB JPEG at
    1376×768, saved as .jpg with the note printed. That run is also what confirms the
    aspect_ratio mapping above.
  • Tests: 5 new vitest cases — missing ATLASCLOUD_API_KEY, unknown provider, refused --ref,
    a full submit → poll → download asserting the submitted aspect_ratio, the prediction URL, the
    explicit User-Agent and the JPEG rename, plus a failed prediction.
    vitest run src/__tests__/generate-image.test.ts19 passed.
  • Types: tsc --noEmit reports nothing for the touched files. (There are pre-existing errors in
    src/agent-farm/*, src/commands/doctor.ts and src/lib/github.ts from unbuilt workspace deps in
    my sandbox — untouched by this PR.)

While writing the test I hit a fixture trap worth mentioning: Buffer.from([...]).buffer is a
pooled ArrayBuffer with a non-zero offset, so slicing it from 0 hands back the wrong bytes and the
JPEG-detection assertion failed for the wrong reason. The test uses new Uint8Array([...]) and says
why in a comment.

Docs

Both skeleton copies (.claude and .codex) get the new flag in the flag list plus a short
"Alternative provider" section with the measured differences. The key is an environment-variable
placeholder; no credentials in the diff.


🤝 Partnership & contact

This PR comes from the Atlas Cloud team. Beyond the integration above, we'd love to explore a closer collaboration with codev — for example co-marketing or a featured integration.

If that sounds interesting, reach out anytime:

And of course, happy to revise this PR to match your project's conventions — just leave a comment. 🙌

`codev generate-image` is wired to the @google/genai SDK, so it needs a Google
AI Studio key. Atlas Cloud serves the same Nano Banana Pro model over its own
submit-then-poll REST API, so this adds it behind `-p/--provider atlas`. The
default stays `gemini` and its code path is untouched.

Measured against the endpoint rather than assumed:

- `-a/--aspect` maps to Atlas's `aspect_ratio` and is honoured: 1:1 returns
  1024x1024 and 16:9 returns 1376x768.
- `-r/--resolution` has no equivalent field there. Rather than silently
  dropping it, a note is printed saying the model's default resolution comes
  back.
- `--ref` is not supported on this path (text-to-image only) and exits with an
  error that points at `--provider gemini`, instead of generating something
  that quietly ignores the reference images.
- The model returns JPEG. Writing those bytes into the default `output.png`
  would mislabel the file, so the output is named after its actual bytes and
  the rename is logged. The Gemini path keeps its existing behaviour.

No new dependency: the Atlas path uses global fetch (the package already
requires Node >= 20).

Tests: 5 new vitest cases covering the missing credential, an unknown provider,
the refused `--ref`, a full submit/poll/download with the JPEG rename and the
explicit User-Agent, and a failed prediction. `vitest run
src/__tests__/generate-image.test.ts` is 19 passed. `tsc --noEmit` reports
nothing for the touched files (the pre-existing errors in `src/agent-farm` and
`src/lib/github.ts` come from unbuilt workspace deps).

One provider detail worth having in the file: api.atlascloud.ai rejects some
clients' default User-Agent with 403 (error code 1010), which is what the
constant and its comment are for.

Signed-off-by: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com>

@waleedkadous waleedkadous left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Welcome, and thank you — this is a well-made first contribution: the flag-mapping table is measured rather than assumed, --resolution/--ref fail honestly instead of silently, the API key appears only in the Atlas Authorization header and never reaches the CDN fetch, and the default gemini path is untouched (all 14 existing tests pass unmodified). A few things need fixing before merge, and since they're all mechanical we'll push them onto your branch ourselves rather than send you a list (maintainer edits are enabled — thank you); your commits and authorship stay as they are. If you'd rather do them yourself, say so and we'll hold off.

What we'll change, in the spirit of the repo's fail-fast rule:

  1. Skill docs: the repo's own tracked .claude/skills/generate-image/SKILL.md and .codex/skills/generate-image/SKILL.md need the same edit as the two skeleton copies (all four are meant to stay byte-identical; packages/codev/skeleton/ is generated and correctly untouched).
  2. Per-request timeouts: ATLAS_TIMEOUT_MS bounds the poll loop but no individual fetch — a stalled submit, poll, or CDN download hangs forever. signal: AbortSignal.timeout(...) on all three.
  3. Unknown status fails fast: the loop only exits on completed/failed, so cancelled, error, or an undefined status polls for the full 300s and then reports a misleading timeout. Allowlist the in-progress statuses and error immediately on anything else, naming it.
  4. Unrecognized download bytes must not be written: withDetectedExtension currently passes unknown magic bytes through, so an HTML error body with a 200 lands in output.png under a green "Image saved". Fail instead.
  5. Tests: the two polling tests sleep for real (measured 10.01s for the file; useRealTimers with no useFakeTimers) — inject the poll interval; restore the env var; and add the one assertion worth locking down: Authorization is sent to Atlas and NOT to the CDN URL.

Minor, may or may not fold in: a typeof url === 'string' guard on outputs[0], and ATLASCLOUD_API_KEY in codev/resources/cloud-instances.md for discoverability.

Review fixes on top of @binyangzhu000-sudo's contribution, pushed to the
branch rather than sent back as a list. The provider design, the measured
flag mapping and the contributor's commit are unchanged.

Skill docs: `.claude/` and `.codex/skills/generate-image/SKILL.md` get the
same edit as the two skeleton copies, so all four are byte-identical again.
Their frontmatter also lists ATLASCLOUD_API_KEY, since that string is what
drives skill selection.

Fail-fast fixes, in the spirit of the repo rule:

- Every request is bounded. ATLAS_TIMEOUT_MS capped the poll loop but no
  individual fetch, so a stalled submit, poll or download hung forever.
  `atlasRequest` puts connect, stream and parse under one AbortSignal.timeout:
  the signal stays armed while the body streams, so a poll answering 200 with
  an HTML error page, or a download aborted mid-stream, now reports a clear
  error instead of escaping as a raw SyntaxError or TimeoutError stack.
- Unknown statuses fail immediately, naming the status. The loop only exited
  on completed/failed, so `cancelled` or an undefined status polled for the
  full 300s and then reported a misleading timeout. The in-progress allowlist
  is `processing` alone: that is what
  https://www.atlascloud.ai/docs/en/predictions documents, and inventing
  plausible extras would be guessing at a contract.
- Unrecognized download bytes are never written. `withDetectedExtension`
  passed them through, so an HTML error body with a 200 landed in output.png
  under a green "Image saved". It now exits; renamed
  `targetPathForImageBytes`, since the old name no longer described it.
- The prediction id gets the same `typeof` guard as the image URL, and the
  overall deadline is checked after the sleep rather than before, so a budget
  that expires mid-sleep does not buy one more request.
- `--ref` is refused before the credential check, so `-p atlas --ref x`
  without a key names the flag rather than the key.

Tests: 19 -> 35 cases, and the file runs in 181ms instead of ~10s (the poll
cadence and overall budget are injectable; the two polling tests no longer
sleep for real). ATLASCLOUD_API_KEY is explicitly cleared per test. New
coverage for submit non-ok, poll non-ok, unknown status, missing and
non-string prediction id, missing and non-string image URL, unrecognized
bytes, an HTML body served as JSON, a mid-stream abort, and the overall
timeout. The security assertion the review asked for is there: Authorization
IS sent to Atlas and is NOT sent to the CDN, read through `Headers` so a
Headers instance cannot hide a leak behind an empty object.

Each fix was checked by reverting it and confirming a test fails.

`pnpm --filter @cluesmith/codev build` and `tsc --noEmit` are clean; the
package suite is 278 files / 5571 passed.
Records the maintainer-edit approach, the CMAP round, and the three findings
that were declined along with the reasoning.
@waleedkadous

Copy link
Copy Markdown
Contributor

Thanks again for this — it's a genuinely nice first contribution, and the parts that were hardest to get right you already got right: the flag mapping is measured against the endpoint rather than assumed, --ref and --resolution are honest about what Atlas can't do, the API key never reaches the CDN fetch, and the default Gemini path is untouched.

As offered in the review, I've pushed the mechanical fixes onto your branch rather than sending you a list. Your commit and authorship are unchanged — nothing was rebased or squashed, mine just sit on top. Here's exactly what changed and why:

1. Skill docs (all four copies). .claude/skills/generate-image/SKILL.md and .codex/skills/generate-image/SKILL.md at the repo root got the same edit you made to the two under codev-skeleton/. Easy one to miss — the repo keeps four tracked copies byte-identical, and only the two under packages/codev/skeleton/ are generated (you were right to leave those alone). I also added ATLASCLOUD_API_KEY to the frontmatter description, since that string is what an agent reads when deciding whether to use the skill.

2. Per-request timeouts. ATLAS_TIMEOUT_MS bounded the poll loop, but the loop only checks its deadline between requests — so a stalled submit, poll, or download hung forever. All three now go through an atlasRequest helper that carries AbortSignal.timeout(...).

One thing worth knowing, because it isn't obvious: the signal stays armed while the body streams, not just until the headers arrive. So the body reads had to move inside the guard too. Otherwise a poll answering 200 with an HTML error page threw a raw SyntaxError from .json(), and an abort landing on .arrayBuffer() threw a raw TimeoutError — both escaping as stack traces with no error message at all. That one came out of the review consultation and is fixed with tests.

3. Unknown statuses fail immediately. The loop only exited on completed/failed, so a cancelled or absent status polled for the full 300s and then reported a timeout — which is a lie about what happened. It now fails at once and names the status. The in-progress allowlist is processing alone: that's exactly what https://www.atlascloud.ai/docs/en/predictions documents, and I didn't want to invent plausible-looking extras.

4. Unrecognized bytes are never written. This was the one real hazard. withDetectedExtension passed unknown magic bytes through, so an HTML error body served with a 200 would land in output.png under a green "Image saved". It now exits instead. Renamed to targetPathForImageBytes, since the old name no longer described what it does.

5. Tests: 19 → 35, and the file runs in 181ms instead of ~10s. The two polling tests were sleeping for real. The poll cadence and overall budget are now parameters with the old constants as defaults, so tests can pass 0 — nothing but tests passes them. ATLASCLOUD_API_KEY is explicitly cleared per test. New cases cover submit/poll non-ok, unknown status, missing and non-string prediction id, missing and non-string image URL, unrecognized bytes, an HTML body served as JSON, a mid-stream abort, and the overall timeout. Plus the assertion worth locking down: Authorization is sent to Atlas and is not sent to the CDN.

Minor: the typeof url === 'string' guard on outputs[0] (and the same on the prediction id); ATLASCLOUD_API_KEY added to codev/resources/cloud-instances.md; the --ref refusal moved ahead of the credential check, so -p atlas --ref x without a key names the flag rather than the key.

Build and tsc --noEmit are clean, and the package suite is 5571 passing. Each fix was verified by reverting it and confirming a test fails, so none of the new tests are decorative.

One thing I deliberately did not change, because it's your call and the maintainers': a reviewer argued that -r 2K/-r 4K on the Atlas path should fail rather than print a note and continue, under the repo's fail-fast rule. There's a real argument there — it submits a billable job that can't honour the flag. But you chose the note deliberately and explained why, so I've left your behaviour intact and raised it separately rather than quietly reversing it.

Since this is a PR from a fork, CI needs a maintainer to approve the workflow run before it executes — that's a GitHub setting for first-time contributors, not anything about your code. Nothing else needed from you.

Thanks for the care you put into this, especially measuring the endpoint instead of guessing at it and writing up what you found. That made the review much easier. Hope to see more from you.

@waleedkadous waleedkadous left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-verified after the maintainer-side commits: all four tracked SKILL.md copies byte-identical; every Atlas fetch goes through one AbortSignal.timeout-guarded helper (including the response-body read, which CMAP caught was outside the guard); the in-progress status allowlist is sourced from Atlas's docs and anything else fails fast naming the status; unrecognized download bytes now refuse to write; 35 tests running in milliseconds, including the key-sent-to-Atlas-never-to-CDN assertion. On --resolution 2K/4K printing a note rather than erroring on Atlas: that was your deliberate, documented design and it stands. CI 7/7 on bfcc2bb. Approving and merging — welcome aboard, @binyangzhu000-sudo, and thank you.

@waleedkadous
waleedkadous merged commit 03bc521 into cluesmith:main Sep 4, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants