Open ChatGPT today, ask it for a picture, and something quiet happens under the hood: you talk to a regular chat model, and it writes the actual prompt for the image model on your behalf, then hands it back to you to refine in conversation. That flow, with the chat model as the operator and the image model as the tool, is now the default way people make images. When I built AgentStudio, it didn’t exist yet. The frontier labs weren’t doing image generation well; the best models were coming out of small startups, each with its own bespoke prompting dialect, and every one of them was operated by typing directly into the model. Nobody had put a chat agent in front of them. So I did: a chat interface where you describe what you want, and the LLM prompts, generates, and edits the image for you across whichever image model you pick.
You talk; the agent operates the image model
You type “a foggy harbor at dawn, cinematic” into a chat, and an image shows up, not as a markdown link dumped into the transcript, but in a dedicated panel next to the conversation, with zoom, rotate, download, and version history. Then you keep talking to it: “make it 16:9,” “warmer light,” “remove the boat.” You never re-attach the previous image or re-type the prompt. The agent already knows what you’re looking at and edits from there.
Underneath, the LLM isn’t returning pixels. It’s deciding that an image artifact should be created or updated and calling a tool to do it. What made this feel smart before it was common was the part that lived in the system prompt: when I built this, every good image model had its own prompting tricks, such as how to phrase a Recraft prompt versus a FLUX one, when guidance helps, and what wording an Ideogram remix wants. I wrote those guidelines into the chat model’s instructions so it would meta-prompt the image model correctly for whichever one you’d chosen. That’s a skill in the modern agent sense: a bundle of model-specific know-how the agent applies on your behalf, before anyone was calling them skills. Today the flow works out of the box because OpenAI and Google trained their chat models and image models together on exactly this handoff. I didn’t have that luxury, so I encoded the handoff by hand.
The LLM plans; the code executes
The cleanest decision in the codebase is where I drew the line between the model and the machine. The LLM gets to interpret intent: it decides make an image or edit this image and emits a structured tool call. It does not get to choose provider parameters. The moment the tool call is formed, deterministic code takes over: it picks the right model, builds the exact provider options, calls the generation API, streams the result back, and persists it.
That boundary exists because image APIs are fragile in inconsistent ways. One model wants image_url, another image_urls. One accepts a guidance scale, another rejects the field entirely. One takes aspect_ratio, another takes a named image_size like landscape_16_9. Trusting a language model to get all of that right on every call is how you ship a flaky product. Keeping it in typed code is how you don’t. So the LLM is the planner for flexible human intent, and everything with a contract, including provider quirks, permissions, persistence, and model routing, stays in application code where it can be tested.
A model-agnostic layer, not a model integration
The reason I could offer “pick your model” as a one-tap choice is that the whole thing routes through a single aggregator API. I used FAL, which fronts the top image models behind one consistent interface, so adding or swapping a model is a config change, not a new integration. On top of that I built a normalization layer the user never sees. Each model has internal capability flags (text-to-image, image-to-image, multi-image), and the router reads intent against them:
- You picked a text-to-image model but attached a photo? It maps you to that family’s image-to-image equivalent.
- You picked an image-to-image model but gave it no image? It maps back to the text-to-image side.
- You’re editing an existing generation with no new upload? It infers image-to-image anyway.
- The model you chose can’t edit at all? It falls back to a capable editing model instead of failing.
Multi-image is the one case where explicit user choice always wins over inference. I don’t silently switch someone onto an expensive multi-image model just because two images happen to be in scope. Above all of that sits a universal vocabulary: the user thinks in 1:1, 16:9, 9:16, and the server translates each into whatever the chosen model actually expects, omitting parameters (like guidance scale) that a given model doesn’t support. Build the product vocabulary first, then adapt to the vendors; never let the vendor API leak into the UX. This is the same product-first instinct I later pushed further in Product Pics and eventually made native to an operator’s workflow in Zodega.
Images are versioned artifacts, not chat messages
My other projects lean systems, with runtimes, queues, and delivery guarantees, and this is where a bit of that rigor goes up into a creative product. A generated image isn’t a transient chat reply here; it’s a durable, typed, versioned document. Text, images, sheets, and slides are all the same conceptual unit: an artifact with a server handler, streaming deltas, UI actions, and history. So the chat route never needs to know the internals of any one kind. Adding a new artifact type is: write a server handler, write a client definition, register both.
The versioning is my favorite piece of schema restraint. Instead of a revision table or an event log, a document’s primary key is (id, createdAt). Every edit writes a new row with the same logical id and a fresh timestamp. Fetching by id returns every version in order; the latest is just the newest timestamp; browsing history is array indexing. That fits image generation perfectly, because generations are immutable snapshots. You almost never want to mutate a previous image; you want to make a new version of it and keep the old one. Append, never rewrite. Undo and history fall out of the data shape for free.
Iteration is a first-class path
The conversational editing I described up top is a specific engineering choice, not a UI trick. When you say “make it warmer” without re-uploading, the server walks back through the conversation’s prior tool results, recovers the most recent generated image, and feeds it into the next image-to-image call as the input. The chat history is the canvas state. There’s no separate “current image” service to keep in sync, because the conversation and the document versions already hold everything needed to iterate. It’s a small simplification with a big payoff: the entire edit loop is stateless infrastructure riding on state that already exists. The honest limit is that this rehydrates image bytes through message history, which is fine for a focused tool, but at real scale you’d pass blob references instead of inline data. Storage makes the same pragmatic trade: images live as base64 in the document, which keeps rendering and history trivially simple and is exactly the kind of thing you’d move to object storage before going wide.
One systems habit that survived the trip up the stack: model access is enforced where the request is served, not where the menu is drawn. The model picker can be wrong, bypassed, or hand-edited, so the chat route independently checks that the selected image model is actually available to the caller and rejects it otherwise, and uploads are auth-gated, type- and size-validated, and stored with random suffixes. UI filtering is a convenience; the route is the boundary.
The stack
Next.js 15 and React 19, TypeScript throughout, Postgres for documents/projects/collections with the append-only version scheme, Vercel Blob for uploads, and the Vercel AI SDK driving a tool-calling chat loop where createDocument/updateDocument dispatch into per-kind artifact handlers. Image generation routes through FAL as a single aggregator over many image models, with a normalization layer for capabilities, aspect ratios, and provider options so the model list can grow without the UI changing. Playwright and route-level tests cover the artifact flows. The architecture is deliberately generic at the core and image-specific only at the edges, including model routing, attachment parsing, provider-option adaptation, and conversational editing, which is why the same bones would carry code files, documents, or slides just as well.
