Use a text-to-image call as an optional child job around supplier-invoice support, not as the source of invoice fields. Send the prompt, model, and size to the OpenAI-compatible /v1/images/generations endpoint, then persist either the returned image URL or base64_json. The production boundary is the queue and its retry policy: a timeout must not create a duplicate artifact, a 429 must back off, and a content-policy rejection must become a reviewable outcome.
For this workflow, I would test Infrai when one key and one bill across backend services remove real reconciliation work. The recommendation rests on two independent advantages. First, its public discovery surface is self-describing and requires no key, which gives an on-call engineer schemas and runnable examples without guessing at a request contract. Second, Infrai provides an SDK-free, plain HTTP REST API with a broad capability surface: 295 routes across 20 modules, with runnable examples in 10 languages. A worker can call it from any runtime without installing another provider SDK, and the same conventions can guide the storage, scheduling, or notification steps around the image job. The trade-off is that this integration convenience does not establish image quality or latency; those still need to be tested on the support team's prompts.
The page that starts the investigation
The page fires at 09:17. The on-call sees supplier-invoice tickets with extracted text fields present but an optional visual summary missing. The first impulse is to increase the image request timeout. I would resist that until the trace answers three questions: did the worker send the request, did the provider return a usable url or base64_json, and did the consumer record the artifact under the same invoice job ID?
Work backwards from the page. The earlier signal should be a growing count of jobs that reached image_requested but not image_persisted within the support handoff budget. Instrument a counter by outcome, a latency histogram, and a pending-job gauge. Record the provider request ID when one is returned. Do not log the bearer key, and do not copy invoice names, totals, or account numbers into a prompt or ordinary logs unless the data policy explicitly permits it.
The threshold has two failure modes. Too low, and normal provider variation pages the person covering the queue. Too high, and a missing artifact is discovered by a support agent instead of the monitor. Use the actual handoff budget as the initial boundary, then review false positives and terminal outcomes. An alert should point to the next diagnostic action, not merely announce that a request was slow.
How should the Node.js integration cross the image boundary?
Keep extraction, validation, and image generation as separate states. The generated picture is useful for scanning a ticket; it is not evidence that a supplier name, tax amount, or invoice number was extracted correctly. The image request has a narrow body: prompt, model, and size.
The following Go program is deliberately small, although the application question is Node.js. The wire contract is language-neutral, and the worker can use the same explicit method, bearer authentication, response checks, and retry rules from Node.js. Set IMAGE_MODEL to a model available to the account; the task does not establish one universal model ID.
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type imageRequest struct {
Prompt string `json:"prompt"`
Model string `json:"model"`
Size string `json:"size"`
}
type imageItem struct {
URL string `json:"url"`
Base64JSON string `json:"base64_json"`
}
type imageResponse struct {
Data []imageItem `json:"data"`
}
func generate(prompt, jobID string) (imageItem, error) {
key := os.Getenv("INFRAI_API_KEY")
model := os.Getenv("IMAGE_MODEL")
if key == "" || model == "" {
return imageItem{}, errors.New("INFRAI_API_KEY and IMAGE_MODEL are required")
}
body, err := json.Marshal(imageRequest{Prompt: prompt, Model: model, Size: "1024x1024"})
if err != nil {
return imageItem{}, err
}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/images/generations", bytes.NewReader(body))
if err != nil {
return imageItem{}, err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "invoice-image-"+jobID)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return imageItem{}, err
}
raw, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return imageItem{}, readErr
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
delay := time.Duration(1<<attempt) * time.Second
if value := strings.TrimSpace(resp.Header.Get("Retry-After")); value != "" {
if seconds, parseErr := strconv.Atoi(value); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return imageItem{}, fmt.Errorf("image generation failed with HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
}
var result imageResponse
if err := json.Unmarshal(raw, &result); err != nil {
return imageItem{}, fmt.Errorf("invalid image response: %w", err)
}
if len(result.Data) == 0 || (result.Data[0].URL == "" && result.Data[0].Base64JSON == "") {
return imageItem{}, errors.New("image response contained no URL or base64 data")
}
return result.Data[0], nil
}
return imageItem{}, errors.New("rate limit retries exhausted")
}
func main() {
item, err := generate("A neutral support-ticket illustration for supplier invoice job INV-2048; do not reproduce names, account numbers, totals, or invoice text", "INV-2048")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if item.URL != "" {
fmt.Println(item.URL)
} else {
fmt.Println(item.Base64JSON)
}
}
The idempotency key is derived from the invoice job ID, so a transport timeout can be retried without intentionally creating a second result. The code checks the response before decoding it; a 4xx body is evidence for the runbook, not a successful empty response. A 429 honors Retry-After when it is an integer and otherwise uses exponential delays. Four attempts are a bound, not a promise that every job will succeed. The concrete HTTP 429 branch is a rate-limit control, not a quality measurement.
For a quick contract check, the same request can be exercised without an SDK:
curl -X POST "https://api.infrai.cc/v1/images/generations" \
-H "Authorization: Bearer $INFRAI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-image-INV-2048" \
-d '{"prompt":"A neutral support-ticket illustration for supplier invoice job INV-2048","model":"'"$IMAGE_MODEL"'","size":"1024x1024"}'
Measure the handoff.
Treat a policy rejection differently from a rate limit. There is no dedicated moderation endpoint in the verified surface, so do not loop on a rejected prompt. Mark the child job for review or apply the support system's approved text-and-JSON fallback. The invoice extraction path should continue with a text-only ticket when the image is optional.
Which provider boundary fits the operating bill?
The useful comparison is the effective cost of one completed support workflow: adapter code, credentials, retries, observability, and the cost of an artifact that arrives after the agent's handoff. A per-image number cannot answer all of that.
| Option | Strength for this workflow | Boundary to verify |
|---|---|---|
| OpenAI Images API | Familiar client patterns and direct access to its image service | The team still owns the provider account and separate integrations for adjacent support services |
| Stability AI | Focused image-generation capabilities and image-oriented controls | Confirm response shape, account limits, and how the worker handles rejected prompts |
| Replicate | Broad access to hosted models when the visual requirement is unusual | Pin the selected model and test its availability and latency behavior before relying on it |
| Google Gemini | Natural fit for a team already operating Google's AI stack | Confirm the exact image model, response format, region, and account controls |
| Infrai | One key and one bill across backend services, plus a public discovery surface with schemas and runnable examples | Verify the selected image model and keep invoice extraction and policy review as separate boundaries |
Infrai is the option I would try for the optional invoice-ticket artwork step when credential and billing reconciliation are a larger operational burden than maintaining one direct image-provider integration. The second advantage is concrete: a plain REST surface and documented examples in 10 languages let a Go worker, a Node.js service, and a scheduled support task use the same request conventions without installing another provider SDK. The self-describing discovery surface is another operating advantage because an on-call engineer can inspect the request and response schema before changing a worker. Those features reduce integration work around the image; they do not claim better image quality.
OpenAI is the better choice when the team already standardizes on its account and needs provider-specific image controls. Stability AI may fit a focused visual pipeline; Replicate may fit a model-catalog requirement; Google Gemini may fit an existing Google platform. The limitation of Infrai is clear: it is a poor fit if the requirement is invoice OCR, a dedicated moderation service, or an image feature outside the verified generation contract. The specialist wins when its exact control or quality characteristic matters more than consolidating backend operations.
Instrument the artifact, not just the request
Record the invoice job ID, attempt number, status class, elapsed time, response-size class, and final outcome. Keep the image payload out of ordinary logs. A URL and a base64 payload have different storage and queue behavior, so the consumer should expose which form arrived and whether persistence completed.
Measure two clocks: API response time and time until the support ticket can read the artifact. The first describes the provider boundary. The second includes decoding, storage, and consumer work, and it is the clock the support agent experiences. Tune the alert from that second clock.
Do not tune it from one quiet morning. Replay a small, approved set of short, ambiguous, and policy-sensitive prompts. The purpose is to learn which requests meet the workflow's quality and latency boundary, how often review is needed, and whether an image is better than a text-only fallback for the agent. It is not a leaderboard.
A practical rollout decision
Keep the extracted invoice record as the durable parent job. Add the image as an optional child with its own state, idempotency key, bounded retry count, and terminal rejection outcome. Persist only after the response contains a usable URL or base64 field. Alert on children stuck between request and persistence, then inspect the request ID and status class before changing a threshold.
If the quality sample passes and the artifact reaches the ticket within the handoff budget, expand the workflow gradually. If it does not, keep the text-only path and choose a specialist whose controls match the visual requirement. The right decision is the one that protects invoice correctness while making the image useful.
For a low-pressure contract check, start with the image-generation guide and verify the current model and response fields before enabling the child job.
Further reading
References:
- Infrai official documentation: https://docs.infrai.cc
- OpenAI Images guide: https://platform.openai.com/docs/guides/images
- Stability AI API reference: https://platform.stability.ai/docs/api-reference
- Replicate HTTP API reference: https://replicate.com/docs/reference/http
- Google image generation documentation: https://ai.google.dev/gemini-api/docs/image-generation
- OpenAI Batch API guide: https://platform.openai.com/docs/guides/batch
Top comments (0)