Let’s make it bullet-proof with a 2-route + client polling flow. This avoids server timeouts and handles any output shape Recraft/Replicate returns.

What we’re changing (simple + reliable)

Route A — create: starts the job, returns predictionId.

Route B — status: checks the job and extracts an SVG URL (if available).

Client: creates → polls status every 2s until succeeded → downloads SVG.

This removes long server waits and makes errors crystal-clear.

1) Server: Create route

src/app/api/vector/recraft/create/route.ts

export const runtime = "nodejs";

import { NextRequest, NextResponse } from "next/server";

const REPLICATE_TOKEN = process.env.REPLICATE_API_TOKEN!;
const VERSION = process.env.REPLICATE_RECRAFT_V3_VERSION || "";

async function getVersion(): Promise<string> {
  if (VERSION && VERSION.length > 16) return VERSION;
  const resp = await fetch("https://api.replicate.com/v1/models/recraft-ai/recraft-v3", {
    headers: { Authorization: `Token ${REPLICATE_TOKEN}` },
    cache: "no-store",
  });
  if (!resp.ok) throw new Error(`Model read failed: ${resp.status} ${await resp.text()}`);
  const json = await resp.json();
  const id = json?.latest_version?.id;
  if (!id) throw new Error("No latest_version.id");
  return id as string;
}

export async function POST(req: NextRequest) {
  try {
    if (!REPLICATE_TOKEN) return NextResponse.json({ error: "Missing REPLICATE_API_TOKEN" }, { status: 500 });

    const { prompt } = await req.json();
    if (!prompt || !prompt.trim()) return NextResponse.json({ error: "Missing prompt" }, { status: 400 });

    const version = await getVersion();

    const createRes = await fetch("https://api.replicate.com/v1/predictions", {
      method: "POST",
      headers: {
        Authorization: `Token ${REPLICATE_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        version,
        input: {
          prompt,
          output_format: "svg",
        },
      }),
    });

    const raw = await createRes.text();
    if (!createRes.ok) {
      return NextResponse.json({ error: `Create failed: ${raw}` }, { status: createRes.status || 502 });
    }

    const data = JSON.parse(raw);
    if (!data?.id) return NextResponse.json({ error: "No prediction id" }, { status: 502 });

    return NextResponse.json({ predictionId: data.id, version });
  } catch (e: any) {
    return NextResponse.json({ error: e?.message ?? "Server error" }, { status: 500 });
  }
}

2) Server: Status route

src/app/api/vector/recraft/status/route.ts

export const runtime = "nodejs";

import { NextRequest, NextResponse } from "next/server";

const REPLICATE_TOKEN = process.env.REPLICATE_API_TOKEN!;

export async function GET(req: NextRequest) {
  try {
    if (!REPLICATE_TOKEN) return NextResponse.json({ error: "Missing REPLICATE_API_TOKEN" }, { status: 500 });

    const id = req.nextUrl.searchParams.get("id");
    if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });

    const stRes = await fetch(`https://api.replicate.com/v1/predictions/${id}`, {
      headers: { Authorization: `Token ${REPLICATE_TOKEN}` },
      cache: "no-store",
    });

    const raw = await stRes.text();
    if (!stRes.ok) {
      return NextResponse.json({ error: `Status failed: ${raw}` }, { status: stRes.status || 502 });
    }

    const data = JSON.parse(raw);
    const status = data?.status ?? "unknown";
    const svgUrl = pickSvgUrl(data?.output);

    return NextResponse.json({
      status,
      svgUrl,
      rawOutputShape: describeShape(data?.output), // for debugging transparency
    });
  } catch (e: any) {
    return NextResponse.json({ error: e?.message ?? "Server error" }, { status: 500 });
  }
}

function pickSvgUrl(output: unknown): string | null {
  if (!output) return null;

  // string
  if (typeof output === "string") {
    if (isSvgUrl(output)) return output;
    return null;
  }

  // array of strings or objects
  if (Array.isArray(output)) {
    const s = output.find(u => typeof u === "string" && isSvgUrl(u));
    if (typeof s === "string") return s;

    for (const it of output) {
      if (it && typeof it === "object") {
        const candidate = (it as any).svg_url || (it as any).svg || (it as any).url || (it as any).uri || (it as any).href;
        if (typeof candidate === "string" && isSvgUrl(candidate)) return candidate;
      }
    }
    return null;
  }

  // object
  if (typeof output === "object") {
    const obj = output as any;
    const cand = obj.svg_url || obj.svg || obj.url || obj.uri || obj.href;
    if (typeof cand === "string" && isSvgUrl(cand)) return cand;
  }

  return null;
}

function isSvgUrl(u: string): boolean {
  const lower = u.toLowerCase();
  return lower.endsWith(".svg") || lower.includes(".svg?");
}

function describeShape(v: any): string {
  if (v == null) return "null";
  if (typeof v === "string") return "string";
  if (Array.isArray(v)) return `array(${v.length})`;
  if (typeof v === "object") return "object";
  return typeof v;
}

3) Client: Create + poll until success

src/components/BrandKit/RecraftGenerator.tsx (drop-in replacement)

"use client";
import React, { useEffect, useMemo, useRef, useState } from "react";

type GenState = "idle" | "running" | "done" | "error";

export type RecraftGeneratorProps = { initialName?: string | null };

export default function RecraftGenerator({ initialName }: RecraftGeneratorProps) {
  const [prompt, setPrompt] = useState("");
  const [status, setStatus] = useState<GenState>("idle");
  const [error, setError] = useState<string | null>(null);
  const [svgUrl, setSvgUrl] = useState<string | null>(null);
  const [predictionId, setPredictionId] = useState<string | null>(null);
  const pollRef = useRef<NodeJS.Timeout | null>(null);

  const namePrefill = useMemo(() => {
    if (!initialName?.trim()) return "";
    return [
      `minimal geometric monoline icon for brand "${initialName.trim()}"`,
      "clean paths, vector, balanced proportions, modern, professional",
      "no text, no letters, logo-ready svg",
    ].join(", ");
  }, [initialName]);

  useEffect(() => {
    if (!prompt.trim()) setPrompt(namePrefill);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [namePrefill]);

  useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current); }, []);

  async function onGenerate() {
    setStatus("running");
    setError(null);
    setSvgUrl(null);
    setPredictionId(null);

    // 1) create
    const createRes = await fetch("/api/vector/recraft/create", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ prompt }),
    });
    const createJson = await createRes.json();
    if (!createRes.ok) {
      setStatus("error");
      setError(createJson.error || "Create failed");
      return;
    }
    const id = createJson.predictionId as string;
    setPredictionId(id);

    // 2) poll
    let elapsed = 0;
    pollRef.current = setInterval(async () => {
      elapsed += 2000;
      const st = await fetch(`/api/vector/recraft/status?id=${encodeURIComponent(id)}`, { cache: "no-store" });
      const js = await st.json();

      if (!st.ok) {
        setStatus("error");
        setError(js.error || "Status failed");
        if (pollRef.current) clearInterval(pollRef.current);
        return;
      }

      if (js.status === "succeeded") {
        if (pollRef.current) clearInterval(pollRef.current);
        if (js.svgUrl) {
          setSvgUrl(js.svgUrl as string);
          setStatus("done");
        } else {
          setStatus("error");
          setError("Model completed but returned no SVG URL");
        }
        return;
      }

      if (js.status === "failed") {
        if (pollRef.current) clearInterval(pollRef.current);
        setStatus("error");
        setError("Generation failed");
        return;
      }

      // timeout after ~3.5 minutes client-side
      if (elapsed >= 210000) {
        if (pollRef.current) clearInterval(pollRef.current);
        setStatus("error");
        setError("Timed out waiting for SVG");
      }
    }, 2000);
  }

  function onClear() {
    setPrompt("");
    setStatus("idle");
    setError(null);
    setSvgUrl(null);
    setPredictionId(null);
    if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
  }

  async function onDownload() {
    if (!svgUrl) return;
    const resp = await fetch(svgUrl, { cache: "no-store" });
    const svgText = await resp.text();
    const blob = new Blob([svgText], { type: "image/svg+xml;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = makeFileName(prompt || initialName || "vector");
    a.click();
    URL.revokeObjectURL(url);
  }

  return (
    <div className="w-full max-w-3xl space-y-4">
      <div className="space-y-2">
        <label className="block text-sm font-medium">Describe your logo</label>
        <textarea
          className="w-full rounded-lg border p-3 leading-relaxed"
          rows={3}
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder='(Blank by default) e.g., "minimal coffee bean icon, vector, clean paths, no text"'
        />
        {initialName?.trim() && (
          <p className="text-xs opacity-70">Prefilled from Wizard: <strong>{initialName}</strong></p>
        )}
      </div>

      <div className="flex items-center gap-3">
        <button
          onClick={onGenerate}
          disabled={status === "running" || !prompt.trim()}
          className="rounded-xl px-4 py-2 font-semibold text-white disabled:opacity-60"
          style={{ background: "#05445e" }}
        >
          {status === "running" ? "Generating…" : "Generate SVG"}
        </button>
        <button onClick={onClear} className="rounded-xl px-4 py-2 font-semibold" style={{ border: "1px solid #777" }}>
          Clear
        </button>
        {svgUrl && (
          <button
            onClick={onDownload}
            className="rounded-xl px-4 py-2 font-semibold"
            style={{ border: "1px solid #05445e", color: "#05445e" }}
          >
            Download SVG
          </button>
        )}
      </div>

      {predictionId && (
        <p className="text-xs opacity-70">Prediction: <code>{predictionId}</code></p>
      )}
      {error && (
        <div className="rounded-lg border border-red-300 bg-red-50 p-3 text-sm text-red-700">{error}</div>
      )}

      <div className="rounded-2xl border p-4">
        <div className="mb-2 text-sm opacity-70">Preview</div>
        <div className="flex items-center justify-center rounded-xl bg-white" style={{ height: 280, border: "1px dashed #d0d7de" }}>
          {status === "idle" && <div className="text-sm opacity-60">No logo generated yet</div>}
          {status === "running" && <div className="text-sm opacity-60">Working…</div>}
          {svgUrl && (
            <img
              src={svgUrl}
              alt="SVG preview"
              style={{ maxWidth: "100%", maxHeight: "100%", objectFit: "contain", display: "block" }}
            />
          )}
        </div>
      </div>
    </div>
  );
}

function makeFileName(s: string) {
  const base = (s || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "vector";
  return `${base}.svg`;
}

Why this will fix it

No server timeouts: the API calls are short; the client does the waiting.

Robust parsing: status route looks for .svg across common shapes.

Clear errors: you’ll see exactly when it’s queued/processing/succeeded/failed.

If it still doesn’t return an SVG URL, tell me what rawOutputShape shows from the status route (you’ll see it in the network response). I’ll extend the parser to whatever shape your account is returning.