Skip to content

Add an openai Images provider (API key, explicit-only) and the billed image knobs - #45

Merged
aldegad merged 9 commits into
mainfrom
land/openai-image-provider
Sep 20, 2026
Merged

aldegad merged 9 commits into
mainfrom
land/openai-image-provider

Conversation

@aldegad

@aldegad aldegad commented Sep 20, 2026

Copy link
Copy Markdown
Owner

What

  • openai image provider: OPENAI_API_KEY only, POST /v1/images/generations (/v1/images/edits with refs), default gpt-image-2.5-flare, --quality low|medium|high|xhigh|max, native transparent background.
  • grok Imagine: --quality auto|low|medium and --resolution 1k|1.5k|2k are carried when requested; providers that cannot honour a knob refuse it instead of dropping it.
  • Subscription first: openai runs only when named with --provider openai. It is never a default, never a guided choice, never a fallback, and the presence of OPENAI_API_KEY changes no route. Grok subscription login still wins over XAI_API_KEY.
  • Every call that runs on an API key prints one line saying it is a per-call charge, including the video path.

Why

A server has no ChatGPT login, so a hosted worker could not produce GPT stills. Individual users keep the subscription routes untouched.

Tests

Full suite: 1721 passed, 1 skipped, 1 failed — the same pre-existing packaging test that fails on main (installed distribution version in the shared venv). +84 new tests, no regressions against main (1637 passed).

No version bump, tag or release in this PR.

A headless container (Modal worker, CI) has no ChatGPT OAuth and no `codex`
CLI, so the GPT image models were unreachable there. This adds a third
backend that calls the REST endpoints directly with an API key:
`POST /v1/images/generations`, or `/v1/images/edits` (multipart) when
reference images are attached.

Field names and enums are the documented Images API surface
(developers.openai.com, 2026-09-20): default model gpt-image-2.5-flare,
`quality` low|medium|high|xhigh|max|auto, `background` transparent|opaque
with `output_format: png` for real alpha (so the provider declares the
`native` transparency strategy), and gpt-image always answers with inline
base64 — `response_format` is not accepted and is not sent. `--aspect-ratio`
maps to a concrete `size` from a table whose every entry satisfies the
documented constraints (sides divisible by 16, ratio inside 1:3..3:1,
655,360..8,294,400 px); a ratio with no exact size is refused rather than
rounded to a nearby one.

`--quality` is new and shared: the CLI carries the vocabulary, each provider
declares the subset it can honour, and a level the backend cannot bill is an
error, not a downgrade — grok says so explicitly until it carries one.

Registering a third provider also fixed a latent registry bug: the workflow
catalog built its provider labels by zipping `PROVIDERS` against a positional
tuple, which would have dropped any new backend from the choice list without
a word. Labels are now a checked mapping, `probe_access` answers for openai
(key present = ready, billing = metered API credit), and the API-billing
question names the provider that would be charged.

Nothing falls back: a missing or empty OPENAI_API_KEY names the variable and
says the codex login will not be used in its place, a rejected key or failed
request publishes nothing, and a transport error is never retried because the
server may already have billed it.

tests/gen/test_openai_provider.py pins all of it over a fake transport
(41 cases): the billed fields per endpoint, every quality level, the size
table against the documented constraints, multipart part order, native alpha,
and the failure paths that must not publish, retry or echo the key.
sprite-gen runs on subscriptions people already pay for; the openai backend
exists for servers and SaaS and is billed per call. Registering it as a
peer of codex and grok quietly made it reachable without anyone choosing it,
which is how a subscription user ends up with an invoice (수홍 2026-09-20:
"우선순위는 구독이래야 해 우리 스프젠 오픈소스는").

It is now explicit-only. `--provider openai` is the single way in:

- `SPRITE_GEN_DEFAULT_PROVIDER=openai` is refused with the reason, so the
  env knob cannot stand a metered backend up as the default.
- The guided flow offers subscription routes only (`GUIDED_PROVIDERS`), so
  openai is never recommended and never savable as a preference. Provider
  labels stay a checked mapping over every registered backend, which is what
  the old positional zip could not do.
- The availability fallback is still codex -> grok and nothing else: a codex
  outage reaches another subscription route or fails loud, never the key.
- Having OPENAI_API_KEY in the environment changes no route by itself.

Every call that spends metered API credit now says so on stderr before the
request leaves — openai always, and grok when no Grok login exists and it
falls to XAI_API_KEY. `--help` and the module docs name openai as the
server/SaaS, per-call route.

Invariants 1, 2, 3 and 5 are pinned by tests (6 new cases), including that
the Grok subscription login still outranks XAI_API_KEY and that a
subscription call prints no billing notice.
…hey do nothing

grok prices an image on quality x resolution, so both now reach the request
body — and only when asked for, leaving a call that names neither byte-identical
to what it was. The billing line names the levels that left, and the report
records them.

The enums are the live server's, not the prose: `high` deserializes into the
shared enum and is then refused per model ("This model only supports the
following quality value(s): low, medium, auto"), while `1.5k` renders although
the capability guide lists only 1k and 2k. An unknown field is not refused at
all, which is why the names are checked locally.

openai and codex refuse a level they cannot carry instead of dropping it from a
body the caller pays for: gpt-image sizes from --aspect-ratio, and image_gen has
no dial at all (codex was silently ignoring --quality since it was added).
…s exposed

Running the two real generations item 3 asks for measured two statements the
code was making from prose rather than from the service.

`resolution: 1.5k` at 1:1 returned 1408x1408, not the 1536 that "the long edge
of the published image (`1k` = 1024px)" implied. The names are grok Imagine's
size tiers, not pixel counts, so the comment, the module docstring and --help
stop asserting a formula nothing in the code computes: the tier goes to the
service verbatim and the service sizes.

The charge notice denied the subscription twice, because the shared prefix
already says it:

  [gen] grok: running on XAI_API_KEY — this is a per-call API charge, not a
  subscription (quality=low, resolution=1.5k), not your Grok subscription —
  `grok login` signs that in.

The grok detail now carries only the remedy, and the notice test locks the
single denial so the two halves cannot drift back into repeating each other.
Images already say on stderr when they run on API credit instead of a
subscription (구독 우선 불변식 5). Video did not, although it resolves the same
credential in the same order and a clip costs far more than a still:

  [gen] video: running on XAI_API_KEY — this is a per-call API charge, not a
  subscription (duration=3s, resolution=480p) — `grok login` signs your Grok
  subscription in.

The line is emitted from `_submit_poll_publish`, the one funnel every submitted
job passes through, so `video`, `video-extend` and `video-edit` are all covered
and an injected credential cannot slip past it — announcing only where
`resolve_credential()` is called would have left that hole. The subscription
route stays silent, and a request refused locally (a bad --duration) never
announces a charge it did not make.

Imagine prices a clip by length x output size, so the knobs that set the amount
ride along with the charge; `video-edit` sends neither and names neither. As
with the grok image notice, the detail carries only the remedy, because the
shared prefix already denies the subscription once.

Not changed: `sprite_gen/video/` (video-set) spawns one `sprite_gen.gen.video`
child per clip with stdout+stderr redirected into `<item>/clip.log`, so each
child's notice is recorded per item rather than shown on the batch terminal.
…tion-first rule

The provider table, the default-provider precedence and the transparency table
in docs/gen.md now carry openai, with one section saying why nothing routes there
on its own: it is named at the call site or it does not run. A second section
owns --quality / --resolution as one shared vocabulary with per-provider subsets,
including the two grok facts the server taught us (a quality the shared enum
accepts and the model then refuses, and an unknown field the service ignores
instead of rejecting).

SKILL.md gains the server/SaaS route as its own row. The six READMEs stop
enumerating two providers. docs/video.md records the stderr notice a clip on
XAI_API_KEY now prints before it uploads.
…otices

Five entries under Unreleased (v2.5.0): the provider itself, the
subscription-first rule that keeps it explicit-only, --quality / --resolution
with their per-provider subsets and the server-measured grok facts, the stderr
charge notice on the XAI_API_KEY video route, and the catalog label mapping that
now fails at import instead of dropping an unlabelled provider.

Version numbers in pyproject.toml and SKILL.md are unchanged; the heading names
the version this lands in, as the release commit converts it.
@aldegad
aldegad merged commit bdac812 into main Sep 20, 2026
2 checks passed
@aldegad
aldegad deleted the land/openai-image-provider branch September 20, 2026 04:29
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.

1 participant