Documentation
¶
Overview ¶
Package imageproxy implements the /v1/images/generations pipeline for the Go rewrite. It ports src/sse/handlers/imageGeneration.js (handleImageGeneration) + open-sse/handlers/imageGenerationCore.js (adapter pattern) + the per-provider imageProviders adapters: generate images via the provider's static image config, normalize the upstream response into the OpenAI {created, data:[{url|b64_json}]} shape (or raw binary when response_format=binary).
Supported in this slice:
- OpenAI-compatible (openai, minimax, openrouter, recraft, xai with bodyFields whitelist, vercel-ai-gateway, venice) — passthrough OpenAI shape.
- Gemini — generateContent with responseModalities ["TEXT","IMAGE"] → candidates[].content.parts[].inlineData.data → {b64_json}.
- Codex — Responses API with tools:[{type:"image_generation",…}], SSE parse → {created, data:[{b64_json}]}.
- Sync image providers (step 5): sdwebui (noAuth local /sdapi/v1/txt2img), comfyui (noAuth local passthrough), huggingface (raw binary → b64_json), stability-ai (core/ultra/sd3 segment, image b64_json).
Deferred (501): antigravity (executor). The handler resolves the provider from body.model (provider/model prefix or bare → openai fallback). The async providers fal-ai / black-forest-labs / runwayml / nanobanana are implemented in step 7 (providers_async.go).
NOT in this slice (separate slices): combo expansion, account-fallback rotation, on-401 token refresh, usage persistence, x-9gouter-connection-id forwarding (the JS handler does echo it for pinning; deferred here).
Index ¶
- Constants
- Variables
- func NewMalformedState(msg string) error
- func NewPollFailed(msg string) error
- func NewUnexpectedHost(host string) error
- func WithTransportMetadata(ctx context.Context, meta TransportMetadata) context.Context
- type AntigravityImageExecutor
- type AntigravityImageRequest
- type AntigravityImageResponse
- type Dependencies
- type HTTPExecutor
- type Handler
- type HostPredicateFunc
- type HostResolver
- type ImageInput
- type LifecycleHostPredicate
- type Logger
- type PollRequestFactory
- type PollResult
- type PollStatus
- type PollStatusParser
- type Request
- type RequestOptions
- type ResolverFunc
- type Result
- type SSRFPolicy
- type TransportMetadata
- type ValidatedHost
Constants ¶
const CloudflareImg2ImgModel = "@cf/runwayml/stable-diffusion-v1-5-img2img"
CloudflareImg2ImgModel is the single Cloudflare img2img JSON model.
const CloudflareInpaintingModel = "@cf/runwayml/stable-diffusion-v1-5-inpainting"
CloudflareInpaintingModel is the single Cloudflare inpainting JSON model.
Variables ¶
var BFLHostPredicate = HostPredicateFunc(func(host string) bool { h := strings.ToLower(hostnameOnly(host)) return h == "api.bfl.ai" || strings.HasSuffix(h, ".bfl.ai") })
BFLHostPredicate allows api.bfl.ai and any *.bfl.ai subdomain (spec).
var CloudflareMultipartModels = []string{
"@cf/black-forest-labs/flux-2-dev",
"@cf/black-forest-labs/flux-2-klein-4b",
"@cf/black-forest-labs/flux-2-klein-9b",
}
CloudflareMultipartModels is the exact legacy FLUX.2 multipart set. These models use multipart/form-data and never accept image/mask inputs.
var ErrDownloadFailed = errors.New("image download failed")
ErrDownloadFailed is the 502-mapped error for a binary-download failure (non-image bytes, size cap, fetch error).
var ErrDownloadFailedPoll = ErrDownloadFailed
ErrDownloadFailedPoll is the download-failure diagnostic used inside the poll path (alias of ErrDownloadFailed in image_security.go, kept exported here so tests can assert against it by identity). Maps to 502.
var ErrNotImage = errors.New("not a recognised image (png/jpeg/webp)")
ErrNotImage is returned when sniffImage cannot identify the bytes as PNG/JPEG/WebP. Callers map it to 502.
var ErrPollTimeout = &pollError{httpStatus: http.StatusGatewayTimeout, msg: "image poll timeout"}
ErrPollTimeout is returned when the overall poll deadline expires before the task reaches a terminal state. Maps to 504.
var FalHostPredicate = HostPredicateFunc(func(host string) bool { h := strings.ToLower(hostnameOnly(host)) return h == "queue.fal.run" || strings.HasSuffix(h, ".fal.run") })
FalHostPredicate allows queue.fal.run and any *.fal.run subdomain (spec).
var RunwayMLHostPredicate = HostPredicateFunc(func(host string) bool { return strings.ToLower(hostnameOnly(host)) == "api.dev.runwayml.com" })
RunwayMLHostPredicate allows exactly api.dev.runwayml.com (spec).
Functions ¶
func NewMalformedState ¶
ErrMalformedState is returned when the poll response body cannot be parsed into a known state. Maps to 502.
func NewPollFailed ¶
ErrPollFailed is returned when the provider reports a terminal failure state. Maps to 502.
func NewUnexpectedHost ¶
ErrUnexpectedHost is returned when a poll/result URL host does not match the provider's allowlist. Maps to 502.
func WithTransportMetadata ¶
func WithTransportMetadata(ctx context.Context, meta TransportMetadata) context.Context
WithTransportMetadata attaches the transport metadata to a request context so the production executor can read it. Tests use it to assert what the usecase passed to the executor.
Types ¶
type AntigravityImageExecutor ¶
type AntigravityImageExecutor interface {
// ExecuteImage performs a non-streaming Antigravity `image_gen` generateContent
// call and returns the raw upstream response body + HTTP status. The adapter
// applies the image envelope (requestType:image_gen, imageConfig, clean model)
// and resolves project ID / OAuth bearer / proxy route before the call.
ExecuteImage(ctx context.Context, req AntigravityImageRequest) (AntigravityImageResponse, error)
}
AntigravityImageExecutor is the image-capable boundary for the Antigravity provider. Unlike the generic HTTPExecutor (which only sees an *http.Request), this interface carries the Antigravity image-specific contract: it accepts a Gemini-shaped `contents` body (text prompt + optional inline image inputs) plus the connection's credentials, and returns the raw Gemini candidates response body for inline-image extraction.
The boundary type is unambiguously image-capable — the method signature is dedicated to image generation (`AntigravityImageRequest`/`Response`), so a generic text executor (provider.Executor / BaseExecutor) can NOT satisfy it. The production adapter (app/wire.go) wraps the real Antigravity executor and preserves OAuth bearer auth, project-ID resolution, refresh/account behavior and the existing connection-aware proxy route; it never puts the credential in the URL (`?key=` is forbidden for Antigravity).
imageproxy never imports the antigravity provider package — the wire adapter bridges. The response carries only the bytes + status the usecase needs to normalize inline image data into the OpenAI {created, data:[{b64_json}]} shape; no provider.Lookup, no repository, no proxy types cross this interface.
type AntigravityImageRequest ¶
type AntigravityImageRequest struct {
Model string // the (possibly suffix-carrying) image model id
Contents []byte // Gemini-shaped {contents:[{role,parts:[{text}|{inlineData}]}]}
Credentials domainProv.Credentials
}
AntigravityImageRequest is the image-specific input to the Antigravity image executor. Contents is a Gemini-shaped `contents` array (role + parts carrying text and optional inlineData for image-edit inputs) built by synthAntigravity. Credentials carry the connection's OAuth token and provider-specific data (projectId, _connectionId, email) the adapter uses for project-ID resolution and proxy routing.
type AntigravityImageResponse ¶
AntigravityImageResponse is the raw upstream result. Body is the Gemini candidates JSON (candidates[].content.parts[].inlineData.data); StatusCode is the HTTP status; Err carries a non-nil error only on transport failure.
type Dependencies ¶
type Dependencies struct {
// Executor is the outbound HTTP boundary. If nil, New creates a fallback
// executor over a plain *http.Client (300s body timeout); production wiring
// injects the policy-aware proxy executor from app/wire.go.
Executor HTTPExecutor
Logger Logger
Config config.Config
// PollInterval is the delay between poll attempts. New sets the production
// default 1500ms when zero; tests pass a short value to avoid sleeping.
PollInterval time.Duration
// PollTimeout is the overall polling deadline. New sets the production
// default 120s when zero; tests pass a short value.
PollTimeout time.Duration
// Resolver resolves a hostname to IPs for the SSRF guard and ValidatedHost
// construction (untrusted image input / binary download URLs). If nil, New
// uses a no-op resolver that fails closed — the production wiring in
// wire.go substitutes a net.LookupIP-based resolver. imageproxy never
// performs real DNS itself.
Resolver HostResolver
// SSRFPolicy is the default-deny egress policy for untrusted image URLs.
// If nil, New uses the production default-deny policy (rejects loopback,
// private, link-local, CGNAT, multicast, metadata, .internal). Tests
// inject a permissive policy so an httptest loopback endpoint can exercise
// the download/redirect path — the production policy is never weakened.
SSRFPolicy SSRFPolicy
// LifecycleHostPredicates overrides the production lifecycle host
// allowlists (BFLHostPredicate, FalHostPredicate, RunwayMLHostPredicate,
// NanobananaHostPredicate) per provider id. When an entry exists for a
// provider, the async adapter uses it instead of the production predicate
// to validate submit-derived poll/result URLs. It is an injectable test
// seam so httptest loopback endpoints can exercise the polling path; the
// production wiring leaves this map nil so the exact documented allowlists
// remain the trust boundary. The production predicates themselves are
// tested directly in image_security_test.go.
LifecycleHostPredicates map[string]LifecycleHostPredicate
// AntigravityExecutor is the image-capable Antigravity provider boundary.
// When nil, FormatAntigravity returns 501 (no production delegation). The
// production wiring (app/wire.go) injects an adapter that wraps the real
// Antigravity executor, preserving OAuth bearer, project-ID resolution,
// refresh/account behavior and the existing connection-aware proxy route.
AntigravityExecutor AntigravityImageExecutor
}
Dependencies wires the imageproxy Handler.
type HTTPExecutor ¶
HTTPExecutor is the outbound HTTP boundary for image generation. The usecase never imports the DB, proxy, or connection packages — it hands each upstream request to an executor with the transport metadata (provider, credentials, connection, lifecycle phase) attached to the request context. The production executor (wired in app/wire.go) resolves connection proxy settings, honours a proxy.ValidatedTarget for untrusted image URLs, and calls proxy.ProxyAwareFetch. Tests substitute a recording executor built over httptest.Server.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler runs the image-generation pipeline.
func New ¶
func New(deps Dependencies) *Handler
New constructs a Handler with sane defaults (300s body timeout fallback executor — image gen can be slow, especially Codex streaming). PollInterval defaults to 1500ms and PollTimeout to 120s (production parity values from the spec); tests pass shorter values.
type HostPredicateFunc ¶
HostPredicateFunc adapts a function to LifecycleHostPredicate.
func (HostPredicateFunc) IsAllowedLifecycleHost ¶
func (f HostPredicateFunc) IsAllowedLifecycleHost(host string) bool
type HostResolver ¶
HostResolver resolves a hostname to one or more IP addresses. The production resolver (wired in app/wire.go) performs real DNS via net.LookupIP; the default resolver in imageproxy is an injectable seam so the usecase never performs network I/O itself. The SSRF guard runs against every returned address; a host that resolves to any disallowed address is rejected before a ValidatedHost is built.
type ImageInput ¶
type ImageInput struct {
// Kind is "data" (inline base64) or "url" (remote HTTPS).
Kind string
// B64 is the decoded base64 bytes for a data: URL (Kind=="data").
B64 string
// URL is the HTTPS URL for a remote input (Kind=="url").
URL string
// MIME is the authoritative MIME sniffed from bytes (PNG/JPEG/WebP).
MIME string
// Host is the SSRF-validated, IP-pinned target for a URL input. The adapter
// passes it to h.do so the production executor (wire.go) pins the dial to
// the validated IP:port. Zero value for data inputs.
Host ValidatedHost
}
ImageInput is one validated image input (data URL or HTTPS URL) produced by the safe input resolver (step 4). The adapter never re-validates.
type LifecycleHostPredicate ¶
type LifecycleHostPredicate interface {
// IsAllowedLifecycleHost reports whether host (the url.URL.Host, i.e. the
// canonical "hostname:port" or "hostname") is a permitted lifecycle
// destination for the provider.
IsAllowedLifecycleHost(host string) bool
}
LifecycleHostPredicate decides whether a host is an allowed destination for a given provider's lifecycle URLs (submit/poll/result/download). Production predicates are exact documented host allowlists (BFL, fal-ai, RunwayML, nanobanana). A test-seam predicate allows a test httptest endpoint so contract tests can exercise the polling path without weakening the production allowlists.
func NanobananaHostPredicate ¶
func NanobananaHostPredicate(baseURL string) LifecycleHostPredicate
NanobananaHostPredicate allows the configured base host. The nanobanana provider does not have a fixed documented host; the operator configures the base URL. This predicate is built per-request from the provider base URL by the adapter (step 7); the exported constructor is provided here so tests and the adapter share one definition.
type Logger ¶
type Logger interface {
Infof(format string, args ...any)
Warnf(format string, args ...any)
Debugf(format string, args ...any)
}
Logger is a minimal log sink.
type PollRequestFactory ¶
PollRequestFactory builds the *http.Request for one poll attempt at pollURL. The adapter attaches auth headers, the provider's host-allowlist predicate (via the request context if needed), and the connection metadata. The factory is called for every attempt so a redirect re-validation can rebuild the request without carrying credentials to a foreign origin.
type PollResult ¶
type PollResult struct {
Status PollStatus
Body []byte
// FinalURL is the last poll URL attempted (redacted by the caller in logs).
FinalURL string
}
PollResult carries the parsed status, the raw response body (for the adapter to extract the result URLs/sample on completion), and the final poll URL.
type PollStatus ¶
type PollStatus string
PollStatus is the provider-local parser's verdict for one poll response.
const ( // PollCompleted: the task finished successfully; the adapter extracts the // result from the response body and stops polling. PollCompleted PollStatus = "completed" // PollPending: the task is still running; the helper waits one interval and // polls again (subject to the overall timeout). PollPending PollStatus = "pending" // PollFailed: the task reached a terminal failure state; the helper stops // and returns ErrPollFailed (mapped to 502). PollFailed PollStatus = "failed" // PollMalformed: the response body could not be parsed into a known state; // the helper stops and returns ErrMalformedState (mapped to 502). PollMalformed PollStatus = "malformed" )
type PollStatusParser ¶
type PollStatusParser func(body []byte) (PollStatus, error)
PollStatusParser inspects one poll response body and returns the provider-local status. It MUST NOT retry; it only classifies. A non-2xx response is handled by the helper before the parser is invoked (the parser only sees 2xx bodies).
type Request ¶
type Request struct {
Ctx context.Context
ProviderID string
Model string
Prompt string
N int
// NSupplied is true when the request body carried an explicit `n` key
// (including n:0). The SDWebUI legacy decision table (step 5) distinguishes
// absent n (→ batch_size:1) from explicit n:0 (→ batch_size:0); other
// adapters ignore it.
NSupplied bool
Size string
Quality string
Style string
ResponseFormat string // "url" (default) | "b64_json" | "binary" (raw image bytes)
OutputFormat string // "png" (default) | "jpeg" | "webp" — used by codex + binary
Background string // codex
Credentials domainProv.Credentials
UserAgent string
PreferredConnectionID string // x-9gouter-connection-id hint; "" → auto-resolve
// Options carries provider-specific optional fields with presence semantics
// (json.RawMessage so missing key ≠ supplied null/""/0). The HTTP handler
// populates it after capability decision; the usecase does not enforce
// provider policy (capability table lives in the handler, step 3).
Options RequestOptions
}
Request is the input to Handle.
type RequestOptions ¶
type RequestOptions struct {
// ImageInputs are the validated image inputs (data URL or HTTPS URL) for
// img2img / inpainting. nil when no image was supplied. Populated by the safe
// input resolver (step 4); until then the handler forwards the raw supplied
// image values via RawImageInputs.
ImageInputs []ImageInput
// Mask is the validated mask input for inpainting. nil when not supplied.
// Populated by the safe input resolver (step 4); until then the handler
// forwards the canonical raw mask via RawMask.
Mask *ImageInput
// RawImageInputs carries the raw, presence-bearing image inputs (the
// `image` and `images` JSON values) as supplied by the client, BEFORE the
// safe input resolver (step 4) converts them into typed ImageInputs. The
// HTTP handler populates this in step 3 so the capability table and the
// adapter probe can observe presence; step 4 will replace it with resolved
// ImageInputs and clear it.
RawImageInputs []json.RawMessage
// RawMask carries the canonical raw mask value (one of mask_image/maskImage/
// mask after alias canonicalization) before the safe input resolver (step 4)
// converts it into a typed Mask.
RawMask json.RawMessage
// Width/Height override Size when set (Cloudflare JSON / some providers).
Width json.RawMessage
Height json.RawMessage
// NegativePrompt, Guidance, Seed, NumSteps, Steps, Strength are the six
// named Cloudflare-ish optional fields. json.RawMessage preserves presence
// and the numeric-zero-vs-null distinction.
NegativePrompt json.RawMessage
Guidance json.RawMessage
Seed json.RawMessage
NumSteps json.RawMessage
Steps json.RawMessage
Strength json.RawMessage
}
RequestOptions holds provider-specific optional image-generation inputs with presence-bearing types so the capability table (handler) can distinguish "absent" from "supplied null/empty/zero". The usecase forwards only the permitted, canonical fields to each provider adapter. Raw JSON is kept for fields whose provider wire shape is not yet fixed (cloudflare/async adapters, steps 6–7); the sync/OpenAI/Gemini/Codex paths ignore it for now.
type ResolverFunc ¶
ResolverFunc adapts a function to HostResolver.
func (ResolverFunc) LookupHost ¶
type SSRFPolicy ¶
type SSRFPolicy interface {
// RejectIP reports whether the resolved IP is forbidden.
RejectIP(ip net.IP) bool
// RejectHost reports whether the textual hostname is forbidden before
// DNS resolution (catches "localhost", ".internal", literal private IPs).
RejectHost(host string) bool
}
SSRFPolicy decides whether an IP or hostname is disallowed for untrusted egress. The production policy (defaultSSRFPolicy) is default-deny: loopback, unspecified, link-local (incl. cloud metadata 169.254.169.254), RFC1918 private, CGNAT 100.64.0.0/10, multicast, the explicit metadata host, and .internal domains are all rejected. Tests inject a permissive policy so an httptest loopback endpoint can exercise the download/redirect path — the production policy is never weakened (spec: "test override upstream origin допускается только через injected endpoint policy in tests; production allowlist не ослабляется").
type TransportMetadata ¶
type TransportMetadata struct {
ProviderID string
ConnectionID string
Credentials domainProv.Credentials
Phase string // "submit" | "poll" | "result" | "input" | "output"
ValidatedHost ValidatedHost
}
TransportMetadata describes one outbound image lifecycle HTTP call. It is attached to the request context by the usecase before Executor.Do and read by the production executor (wire.go). It carries no DB/proxy types — only the primitive identifiers and the validated target the policy-aware proxy transport (step 1) needs.
func TransportMetadataFromContext ¶
func TransportMetadataFromContext(ctx context.Context) (TransportMetadata, bool)
TransportMetadataFromContext returns the metadata attached to the context, if any. Used by the production executor in wire.go.
type ValidatedHost ¶
ValidatedHost is the untrusted-image egress contract handed to the policy-aware proxy transport. It mirrors proxy.ValidatedTarget but lives in the usecase package so imageproxy never imports the proxy package; the wire adapter translates it to proxy.ValidatedTarget and attaches it to the request context. Zero value (nil IP / empty port) means "no pinned target — use the standard proxy pipeline" (provider lifecycle requests that are operator-trusted hostnames).
func (ValidatedHost) IsPinned ¶
func (v ValidatedHost) IsPinned() bool
IsPinned reports whether the validated host carries a resolved IP to pin.