imageproxy

package
v0.8.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 19 Imported by: 0

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

View Source
const CloudflareImg2ImgModel = "@cf/runwayml/stable-diffusion-v1-5-img2img"

CloudflareImg2ImgModel is the single Cloudflare img2img JSON model.

View Source
const CloudflareInpaintingModel = "@cf/runwayml/stable-diffusion-v1-5-inpainting"

CloudflareInpaintingModel is the single Cloudflare inpainting JSON model.

Variables

View Source
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).

View Source
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.

View Source
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).

View Source
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.

View Source
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.

View Source
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.

View Source
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).

View Source
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

func NewMalformedState(msg string) error

ErrMalformedState is returned when the poll response body cannot be parsed into a known state. Maps to 502.

func NewPollFailed

func NewPollFailed(msg string) error

ErrPollFailed is returned when the provider reports a terminal failure state. Maps to 502.

func NewUnexpectedHost

func NewUnexpectedHost(host string) error

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

type AntigravityImageResponse struct {
	Body       []byte
	StatusCode int
	Err        error
}

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

type HTTPExecutor interface {
	Do(req *http.Request) (*http.Response, error)
}

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.

func (*Handler) Handle

func (h *Handler) Handle(ctx context.Context, req Request) Result

Handle dispatches the image-generation upstream call by the provider's static config.

type HostPredicateFunc

type HostPredicateFunc func(host string) bool

HostPredicateFunc adapts a function to LifecycleHostPredicate.

func (HostPredicateFunc) IsAllowedLifecycleHost

func (f HostPredicateFunc) IsAllowedLifecycleHost(host string) bool

type HostResolver

type HostResolver interface {
	LookupHost(ctx context.Context, host string) ([]net.IP, error)
}

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

type PollRequestFactory func(ctx context.Context, pollURL string) (*http.Request, error)

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

type ResolverFunc func(ctx context.Context, host string) ([]net.IP, error)

ResolverFunc adapts a function to HostResolver.

func (ResolverFunc) LookupHost

func (f ResolverFunc) LookupHost(ctx context.Context, host string) ([]net.IP, error)

type Result

type Result struct {
	StatusCode  int
	Err         error
	Body        []byte
	ContentType string
}

Result is the output of Handle.

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

type ValidatedHost struct {
	Scheme   string
	Hostname string
	Port     string
	IP       net.IP
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL