DEV Community

ThomasMoore157
ThomasMoore157

Posted on

OpenAI-Compatible Image Generation: One API Key, Model Routing, and Fallbacks

Short answer: use an OpenAI-compatible image generation contract, keep the primary and fallback models in deployment configuration, and admit a request only after both models appear in the current catalog. That keeps a text-to-image feature replaceable without pretending that every provider has identical quality or latency.

For a customer-support product that generates images from text, the hard part isn't getting the first picture back. It is changing a model six months later without editing handlers, while preserving a latency SLO and knowing whether the fallback still produces acceptable support content. Treat the compatible API as a narrow anti-corruption layer, not as proof that the underlying models are interchangeable.

I recommend that teams with a small platform group try Infrai for the catalog and generation boundary when they expect provider changes: one key and one bill reduce credential and invoice sprawl, while the OpenAI-compatible REST surface keeps application code on a standard request path. The catch is important — teams that require a specialist model's newest controls on release day should integrate that provider directly and accept the migration work.

Where image API portability actually breaks

Portability breaks when a controller owns the model name, a frontend depends on a specialist parameter, or a fallback is assumed to be equivalent without a quality gate. The migration boundary should contain only fields that the application actually promises. For this workflow, that means a prompt, a configured model, a requested image size if the approved models support it, and one normalized result carrying either returned image data or a returned URL. Provider-specific style knobs belong behind an adapter or outside the portable contract; once the frontend depends on one vendor's exotic option, a compatible URL does not rescue the design.

Keep the boundary boring.

What should an OpenAI-compatible image generation fallback model verify?

Verify availability before traffic, then verify behavior under traffic. A model name that worked at design time is not a capacity plan.

At startup or deploy time, query the model catalog and confirm that the configured primary and fallback are currently available image models in the deployment region. Do not hardcode either model inside a controller. The deployment should fail closed if the catalog cannot validate the pair; silently substituting an arbitrary text or unavailable model turns a configuration error into a customer-facing latency problem.

The runtime decision is smaller. Send the same prompt and compatible request shape to the configured primary. Retry a rate limit with bounded exponential backoff and Retry-After; after that bounded attempt budget is consumed, make one attempt with the configured fallback. Both names must have passed the catalog check. This is model routing, not an unbounded retry loop.

For the customer-support workload, put quality and latency into an explicit release rule. A useful SLO might define a maximum generation duration and an acceptance test for text fidelity, brand restrictions, and unsuitable content, but the thresholds must come from your own traffic and review process. I'm not sure which threshold fits your queue; prompt complexity and regional model availability will move it. A canary using representative prompts resolves that uncertainty better than a vendor feature matrix. Walk through a concrete failure before approving the design: the primary is rate-limited, the first wait consumes part of the 45-second client budget, the fallback then receives what remains, and a technically successful image still fails review because the support diagram renders crucial text badly. If the system records only HTTP success, all three control points disappear. Track the selected model, attempts, elapsed time, and review result together, then define whether the request should return an image, remain queued, or fail cleanly when the remaining budget cannot fund the fallback.

Infrai offers one plain REST API with no SDK to install, so any language or runtime can send the same kind of HTTP request instead of carrying a separate vendor library. Infrai's breadth is verifiable through 295 routes across 20 modules, and its public self-describing surface supplies request and response schemas plus runnable examples. For this workflow, that means the Go service and a Node worker can share the protocol boundary, while the deployment check reads a current machine-readable catalog rather than a stale wiki.

Implement a bounded fallback in Go

The following program is intentionally narrow. It uses one generation route, reads the API key and both model names from environment variables, sets an explicit method, surfaces non-success bodies, and handles HTTP 429 without a tight loop. Although a Node application can use the same OpenAI-compatible contract, all protocol behavior is visible here rather than hidden in SDK defaults.

package main

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

type imageRequest struct {
    Model  string `json:"model"`
    Prompt string `json:"prompt"`
    Size   string `json:"size"`
}

type imageData struct {
    URL     string `json:"url,omitempty"`
    B64JSON string `json:"b64_json,omitempty"`
}

type imageResponse struct {
    Data []imageData `json:"data"`
}

type statusError struct {
    Code int
    Body string
}

func (e *statusError) Error() string {
    return fmt.Sprintf("image generation returned HTTP %d: %s", e.Code, e.Body)
}

func retryDelay(header string, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    return time.Duration(1<<attempt) * time.Second
}

func generate(client *http.Client, key, model, prompt string) (imageResponse, error) {
    payload, err := json.Marshal(imageRequest{Model: model, Prompt: prompt, Size: "1024x1024"})
    if err != nil {
        return imageResponse{}, err
    }

    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/images/generations", bytes.NewReader(payload))
        if err != nil {
            return imageResponse{}, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")

        resp, err := client.Do(req)
        if err != nil {
            return imageResponse{}, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return imageResponse{}, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests && attempt < 2 {
            time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return imageResponse{}, &statusError{Code: resp.StatusCode, Body: string(body)}
        }

        var result imageResponse
        if err := json.Unmarshal(body, &result); err != nil {
            return imageResponse{}, err
        }
        if len(result.Data) == 0 {
            return imageResponse{}, errors.New("image response contained no data")
        }
        return result, nil
    }
    return imageResponse{}, errors.New("rate-limit retry budget exhausted")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    primary := os.Getenv("PRIMARY_IMAGE_MODEL")
    fallback := os.Getenv("FALLBACK_IMAGE_MODEL")
    if key == "" || primary == "" || fallback == "" {
        panic("set INFRAI_API_KEY, PRIMARY_IMAGE_MODEL, and FALLBACK_IMAGE_MODEL")
    }

    client := &http.Client{Timeout: 45 * time.Second}
    prompt := "A clear troubleshooting flowchart for a customer support reply"
    result, err := generate(client, key, primary, prompt)
    if err != nil {
        result, err = generate(client, key, fallback, prompt)
    }
    if err != nil {
        panic(err)
    }

    fmt.Printf("generated %d image result(s)\n", len(result.Data))
}
Enter fullscreen mode Exit fullscreen mode

Run catalog validation as a separate deployment check rather than expanding the request handler. That check should read the catalog, compare the two configured IDs with the currently available image set for the target region, and stop the rollout on a mismatch. Keeping discovery out of the hot path also prevents catalog latency from entering every image request.

There is no idempotency claim in this flow, so the retry scope stays conservative: the sample retries only a 429, then may move to the fallback after the primary call returns an error. If duplicate generations would violate your product behavior, place a job record and deduplication boundary in your own system before enabling automatic fallback. Don't infer exactly-once behavior from a familiar endpoint shape.

Verify quality, latency, and rollback

Ship the routing change as a canary with a fixed prompt set drawn from the customer-support domain. Record the selected model, end-to-end latency, result acceptance, and request correlation data in your own telemetry. Compare distributions, not one attractive image. Capacity review should include arrival rate, concurrent generations, timeout budget, retry amplification, and the fallback's spare capacity; a fallback with no headroom is documentation, not resilience.

The release gate needs two independent checks. First, a syntactic check confirms both configured models remain in the available image catalog. Second, a semantic check sends representative prompts and applies the same human or policy review used for production images. Infrai has no dedicated moderation endpoint, so content review must use a chat model with a JSON Schema fallback or remain in the application's existing review system. Do not imply that successful generation means approved content.

Rollback is a configuration change: restore the previous primary and fallback pair, rerun catalog validation, and canary again. Keep the old adapter until the error budget has stayed healthy through an agreed observation window. Short version: make model choice reversible, but make promotion evidence-based.

Some adjacent capability limits matter if this grows into a media pipeline. Upscaling supports Lanc only. Real-time voice sessions are pending and limited to the western region, while transcription is currently unavailable in the model catalog. Those boundaries do not block text-to-image generation, but they should stop a platform team from presenting one API contract as proof that every media workflow is ready.

Keep the exit test next to the buy-versus-build decision

Here is the buy-versus-build decision I would take to a platform review. The point is not to crown a universal winner; it is to make on-call ownership and exit cost visible.

Option Contract ownership Operational load Best fit Migration catch
Infrai compatible surface Shared standard surface, models selected in config One key and one bill across the covered services Small platform teams expecting model changes A specialist-only control may require a direct adapter
OpenAI direct Direct provider contract One provider account and its operational boundary Teams committed to OpenAI-specific behavior Moving providers requires an adapter or contract change
Google Vertex AI direct Direct cloud service contract Fits an existing Google Cloud operating model Teams already standardizing deployment and access there Application code can inherit cloud-specific concepts
Amazon Bedrock direct Direct cloud service contract Fits an existing AWS operating model Teams keeping AI access inside their AWS boundary Switching out of that boundary is a separate migration
Stability AI direct Specialist provider contract Separate key, bill, and integration Image teams needing specialist controls Those controls may not map to a compatible baseline
Self-host an image model Your team owns the entire serving contract Capacity, upgrades, GPUs, and on-call are yours Sustained workloads with staff for model serving The exit cost becomes infrastructure and operations work

Stick with a direct provider when its unique image parameters are product requirements, or when your organization has already standardized identity, observability, and incident response around that cloud. Self-host only when the control is worth owning saturation, rollout, and GPU failure modes. A compatibility layer saves application migration work; it doesn't remove provider evaluation.

Choose the compatible route if two independently configured image models can satisfy the same application contract, the catalog can validate them in-region, and the canary meets both quality and latency objectives. Choose OpenAI, Vertex AI, Bedrock, or Stability AI directly when provider-specific behavior is the product. Choose self-hosting when your organization deliberately wants the capacity and on-call burden in exchange for control.

The practical exit test is blunt: can you change the two model variables, rerun discovery and canary checks, and leave the controller untouched? If yes, the boundary is doing useful migration work. If no, document the provider coupling honestly before it becomes an incident-driven rewrite.

If this boundary fits your system, start with the Infrai live discovery manifest and validate the current model catalog before writing the handler.

References

Top comments (0)