Skip to content
C.W.K.
Lesson 02 of 06 · published

provider=auto, Owned by Ember

~15 min · routing, cost order, single source of truth, ember

Level 0Sketch
0 XP0/43 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

One order, one owner

Most image requests in the family name no generator. Pippa's tool and the image-gen skill default to provider=auto, and so does the studio. Behind that word is a cost order Dad set on 2026-09-30: first the shared Codex subscription, then ElevenLabs' prepaid credits, then a local instruction model on Ember's machine, and only then a metered API key. That order lives in one service, Ember's generator catalog, and inside it in one function: auto_route() builds the route by appending the four tiers in exactly that sequence, and each entry's position is the rule. The module also declares AUTO_TIERS = ("codex", "elevenlabs", "local", "api"), but nothing reads that tuple. It is a second statement of the order, true only while someone keeps it in step by hand.

This lesson shows what Ember's code says today so you can see the mechanism. The image-gen skill tells every brain never to restate that order in its own words, for a good reason: the day Ember changes it, every restated copy becomes a quiet lie. Avatars follows that rule in code. It keeps no list of tiers to decide with. Each batch does store a snapshot of the catalog it was planned against, Ember's route included, as a record; nothing reads it back to choose anything.

TierRow Ember resolves toBillingReference limit
codexCodex Image-Gen Tool (codex_image_gen)subscriptionnone declared
elevenlabsthe ElevenLabs image model Ember is configured withcreditsper model
localan installed instruction model on Emberincludedper model family
apiOpenAI Image 2.5 Flare (openai_flare)metered16

One small mismatch inside Ember itself: its module docstring speaks of a metered Google/OpenAI key, while the route's last tier is the OpenAI row only.

How Ember decides

resolve_auto walks the tiers in order and skips a tier for one of four named reasons: it is unavailable (Ember says why), it does not offer the requested output size, the request carries more references than the tier declares it accepts, or it does not offer the aspect ratio. The first tier that survives wins. The function returns the provider and a decision record: what was requested, what it resolved to, which tier, and every tier it passed over with the reason. If nothing survives, it raises No automatic tier can serve this request, lists each tier's reason, and ends with Choose a provider explicitly.

Two details matter. A size of auto, or no size, never disqualifies a tier; it means the chosen tier's own default output. And ElevenLabs with low or unknown credits is skipped by the route while its row stays usable when someone picks it by name.

The route is the first row

Ember publishes the route twice. The tiers, with readiness and limits, go out as auto_route. The route also becomes the first row of the generator catalog, labelled Automatic, and the catalog's default provider becomes auto. Any client that just renders the catalog inherits Dad's order with no code of its own. Avatars' studio renders that row like any other, and under it prints Ember's cost order with unavailable tiers marked, plus each unavailable tier's reason.

Avatars' Review generation window: one native image from one generator and one candidate, a chip reading Codex Image-Gen Tool · auto · subscription, the line Automatic → Codex Image-Gen Tool (codex tier), a note that originals and history are saved in Ember and then copied into Avatars, the prompt sent to Ember with Pippa's identity rules, and the buttons Back to edit and Generate images.
The review before anything is spent. The Automatic row has already been asked where it would go right now: Codex Image-Gen Tool, its codex tier, on a subscription. The prompt Ember will receive sits below, identity rules included.

Ask now, record it, submit the answer

When a request names the automatic generator, plan() does not send auto to Ember and hope. It calls Ember's dry decision, POST /api/v1/api-generators/route, with the request's size, reference count, aspect ratio and quality. Ember's own docstring for that route: which tier an automatic request would take right now, and why the others were skipped, so a client can show it before spending anything. Avatars then swaps in the resolved provider and stores the decision on every task as route. The plan summarises the distinct decisions as routes, and the review step prints one line per decision: Automatic → the chosen row's label and its tier, then skipped with each passed-over row and its reason.

The original choice is not lost. The task keeps provider_options.id == "auto" while provider holds the tier that was chosen, and a test checks both on the stored task. The worker later submits that resolved provider explicitly, so Ember never routes a submitted task a second time. Nothing switches provider after submission, and Ember's resolver says the same from its side: a paid or credited call is never repeated on another provider silently.

Because Generate images plans again, the decision stored is the one made at submission time, which can differ from the one shown minutes earlier. If Codex went down in between, the stored route says so.

The copy that lived 65 minutes

The first version of this was a copy. At 14:32 on 2026-09-30 the studio gained its own Automatic entry, built in the web UI with its own list of sizes. At 15:37 the same day it was gone: Ember now listed the route as its first catalog row and default, so the studio stopped building its own. The commit's subject is the lesson: render Ember's automatic generator from the catalog instead of a local copy. What survives in Avatars is one guarded line for an Ember that publishes no auto_route at all, which picks Ember's static default, or its OpenAI default when 2K or 4K is asked for. It is a fallback for an older engine, and the route is Ember's.

One number to read carefully

Avatars sends the route reference_count=min(inputs, 10). The ten looks like a residue of the ten-reference ceiling removed on 2026-10-06, and the architecture doc says the route gets the full count, soul references included. Nothing records whether the clamp is intended. Its effect is bounded: the per-task check after routing still refuses a tier whose declared limit is too small, by name, before anything is queued, so it cannot overspend. When the Codex tier is up it changes nothing, because Codex declares no limit. With Codex down and 12 references, though, the route can land on a tier that accepts 10 and the plan then refuses, where the metered tier (16) could have taken it, and the skipped text reports 10 where there were 12.

Code

cwkEmber, backend/api_generators/catalog.py: the first eligible tier and its decision record (verbatim, lines 164-186)·python
def resolve_auto(route: list[dict], *, image_size: str | None, reference_count: int, aspect_ratio: str | None = None) -> tuple[str, dict]:
    """The first tier that can serve this request, and the decision record.
    Nothing here retries a tier that failed at generation time: a paid or
    credited call is never repeated on another provider silently."""
    # No size (or "auto") means the chosen tier's own default output; only a
    # requested tier (1K/2K/4K/512) can disqualify a tier.
    size = None if image_size in (None, "auto") else image_size
    considered = []
    for tier in route:
        skipped = None
        if not tier["available"]:
            skipped = tier["reason"]
        elif size is not None and size not in tier["image_sizes"]:
            skipped = f"{size} output is not offered"
        elif tier["max_references"] is not None and reference_count > tier["max_references"]:
            skipped = f"{reference_count} references exceed its limit of {tier['max_references']}"
        elif aspect_ratio and tier.get("aspect_ratios") and aspect_ratio not in tier["aspect_ratios"]:
            skipped = f"{aspect_ratio} output is not offered"
        if skipped is None:
            return tier["provider"], {"requested": "auto", "resolved": tier["provider"], "tier": tier["tier"], "considered": considered}
        considered.append({"tier": tier["tier"], "provider": tier["provider"], "skipped": skipped})
    detail = "; ".join(f"{c['tier']} ({c['provider']}): {c['skipped']}" for c in considered)
    raise ValueError(f"No automatic tier can serve this request: {detail}. Choose a provider explicitly.")
cwkAvatars, backend/studio.py: ask Ember for the decision during the plan (verbatim, lines 405-419)·python
        for provider in body.providers:
            provider_id = catalog.get("aliases", {}).get(provider.id, provider.id)
            route = None
            if provider_id in {"auto", "default", "automatic"}:
                # Ember owns the automatic route (Codex, ElevenLabs credits,
                # local model, metered API). Avatars asks for the decision
                # now, records it, and submits the resolved provider, so a
                # reference-bound request that a tier cannot take is skipped
                # visibly here rather than switched silently later.
                inputs = len(refs) + (1 if body.workflow in {"remix", "regenerate"} else 0)
                if catalog.get("auto_route"):
                    decision = await self.upstream.route(image_size=provider.image_size, reference_count=min(inputs, 10), aspect_ratio=body.aspect_ratio, quality=provider.quality)
                    provider_id, route = decision["provider"], decision.get("decision")
                else:  # an Ember without the tiered route publishes only its static rule
                    provider_id = catalog.get("default_openai_provider") if provider.image_size in {"2K", "4K"} else catalog.get("default_provider")
cwkEmber, backend/api_generators/catalog.py: the route becomes the first row and the default (verbatim, lines 235-242)·python
    catalog["auto_route"] = auto_route(catalog, settings)
    # The route is also the first catalog row and the default: a client that
    # only renders this catalog inherits Dad's order with no code of its own.
    catalog["providers"].insert(0, automatic_row(catalog["auto_route"]))
    for alias in (AUTO_PROVIDER, "default", "automatic"):
        catalog["aliases"][alias] = AUTO_PROVIDER
    catalog["default_explicit_provider"] = catalog["default_provider"]
    catalog["default_provider"] = AUTO_PROVIDER

External links

Exercise

Find a capability in your own system that more than one backend can serve: two payment processors, several model providers, a cheap and an expensive storage tier. Write the preference order as data in exactly one service. Give that service a resolve function that returns both the choice and a record of every option it skipped with the reason, and a dry-run endpoint that answers without doing the work. Then change one client so it asks the dry run while planning, stores the record next to the work item, and submits the resolved backend by name.
Hint
The decision record is what makes the order auditable. Months later, 'why did this job go to the expensive tier?' should be answerable from the job itself. If a client needs the order to draw its UI, publish it as data from the owner; never let the client keep its own copy.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.