Implements the Replicate API backend (FLUX schnell / FLUX dev) per ImaGen issue #3: - internal/backend/replicate.go — Backend adapter. Supports model refs as "owner/name" (uses /v1/models/{owner}/{name}/predictions) and "owner/name:hash" (uses /v1/predictions with explicit version). Polls /v1/predictions/{id} every 500ms with model-aware timeout (60s schnell, 120s dev). Resilience: 401 names api_token_env, 429 with exp backoff up to 3 retries (honours Retry-After), 5xx retries once, image download retries once on transient failure. - internal/backend/replicate_pricing.go — hardcoded per-image USD rates for known FLUX models, snapshotted from replicate.com/pricing with a refresh TODO. - internal/backend/replicate_test.go — mocked-HTTP unit tests covering happy path (model + version-pinned), 401, 429 retry policy, failed prediction, poll timeout, image-download retry, ctx cancel, BackendOpts passthrough, default_steps, aspect-ratio reduction, sha256 prompt hash. - internal/usage/usage.go — Supabase REST sink + read-side query for mai.imagen_usage. Adapter writes are best-effort: failures warn but the image still lands. - cmd/imagen/usage.go — `imagen usage [--since DATE] [--raw]` reads the table and prints a tab-aligned grouped or raw table with totals. - cmd/imagen/backends.go — instances of type=replicate now report "ok" or "not configured (set REPLICATE_API_TOKEN)" depending on env. - internal/config/config.go — sample adds flux-schnell-replicate + flux-dev-replicate; default_backend stays flux-schnell-local. - Supabase migration mai.imagen_usage (id, created_at, backend, model, seed, prompt_hash, latency_ms, cost_usd_estimate, caller) + indexes on (created_at DESC) and (caller). The raw prompt is never stored. Caller identity resolves from MAI_FROM_ID, then the tmux pane's @mai-name option, mirroring the maimcp identity logic. Prompt hash is sha256 of the user-facing prompt; raw prompt never reaches the table.
70 lines
1.7 KiB
Go
70 lines
1.7 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"text/tabwriter"
|
|
|
|
"mgit.msbls.de/m/ImaGen/internal/backend"
|
|
"mgit.msbls.de/m/ImaGen/internal/config"
|
|
)
|
|
|
|
// instanceStatus checks adapter-specific preconditions (e.g. the
|
|
// Replicate API token env var being set) and returns a short
|
|
// user-facing status string.
|
|
func instanceStatus(spec config.BackendSpec) string {
|
|
if !backend.Default.Has(spec.Type) {
|
|
return fmt.Sprintf("type %q not compiled in", spec.Type)
|
|
}
|
|
switch spec.Type {
|
|
case backend.ReplicateType:
|
|
envName, _ := spec.Raw["api_token_env"].(string)
|
|
if envName == "" {
|
|
envName = "REPLICATE_API_TOKEN"
|
|
}
|
|
if os.Getenv(envName) == "" {
|
|
return fmt.Sprintf("not configured (set %s)", envName)
|
|
}
|
|
return "ok"
|
|
}
|
|
return "registered"
|
|
}
|
|
|
|
func runBackends(args []string) error {
|
|
fs := flag.NewFlagSet("backends", flag.ContinueOnError)
|
|
var configPath string
|
|
fs.StringVar(&configPath, "config", "", "config file path (default: ~/.config/imagen.yaml)")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg, cfgErr := config.Load(configPath)
|
|
if cfgErr != nil && !os.IsNotExist(cfgErr) {
|
|
return cfgErr
|
|
}
|
|
|
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(tw, "INSTANCE\tTYPE\tSTATUS")
|
|
if cfg != nil {
|
|
for name, spec := range cfg.Backends {
|
|
status := instanceStatus(spec)
|
|
marker := ""
|
|
if name == cfg.DefaultBackend {
|
|
marker = " (default)"
|
|
}
|
|
fmt.Fprintf(tw, "%s%s\t%s\t%s\n", name, marker, spec.Type, status)
|
|
}
|
|
}
|
|
if cfg == nil {
|
|
for _, t := range backend.Default.Types() {
|
|
fmt.Fprintf(tw, "%s\t%s\t%s\n", t, t, "no config — type registered, no instance defined")
|
|
}
|
|
}
|
|
if err := tw.Flush(); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(os.Stderr, "registered types:", backend.Default.Types())
|
|
return nil
|
|
}
|