Add an openai Images provider (API key, explicit-only) and the billed image knobs - #45
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
openaiimage provider:OPENAI_API_KEYonly,POST /v1/images/generations(/v1/images/editswith refs), defaultgpt-image-2.5-flare,--quality low|medium|high|xhigh|max, native transparent background.--quality auto|low|mediumand--resolution 1k|1.5k|2kare carried when requested; providers that cannot honour a knob refuse it instead of dropping it.openairuns only when named with--provider openai. It is never a default, never a guided choice, never a fallback, and the presence ofOPENAI_API_KEYchanges no route. Grok subscription login still wins overXAI_API_KEY.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.