Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Integrate Replicate AI models with background processing, S3 storage, and credit systems
.claude/skills/aiskillstore-ai-handler/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | -5% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 32% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 10% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 1% | 0% |
| case-08 | ✗→✓ | ▲ Improved | -29% | 0% |
This skill provides a production-ready pattern for integrating Replicate AI models. It handles long-running predictions using Inngest background jobs, stores results in S3, manages user credits, and updates database state.
/api/app/ai-images).status: "processing".status: "completed".failed.replicate package installed (npm install replicate).REPLICATE_API_TOKEN in .env.src/app/api/app/generate/route.ts
typescriptimport withAuthRequired from "@/lib/auth/withAuthRequired"; import { db } from "@/db"; import { generations } from "@/db/schema"; import { inngest } from "@/lib/inngest/client"; import { checkCredits, deductCredits } from "@/lib/credits"; // Hypothetical helpers export const POST = withAuthRequired(async (req, { session }) => { const body = await req.json(); // 1. Check Credits const hasCredits = await checkCredits(session.user.id, "image_generation", 1); if (!hasCredits) return new Response("Insufficient credits", { status: 403 }); // 2. Create DB Record (Pending) const [record] = await db.insert(generations).values({ userId: session.user.id, prompt: body.prompt, status: "processing", }).returning(); // 3. Deduct Credits (Optimistic) await deductCredits(session.user.id, "image_generation", 1, { source: "api", refId: record.id }); // 4. Trigger Background Job await inngest.send({ name: "app/ai.generate", data: { generationId: record.id, prompt: body.prompt, userId: session.user.id } }); return Response.json({ id: record.id, status: "processing" }); });
src/lib/inngest/functions/app/ai/generate.ts
typescriptimport { inngest } from "@/lib/inngest/client"; import Replicate from "replicate"; import uploadFromServer from "@/lib/s3/uploadFromServer"; import { db } from "@/db"; import { generations } from "@/db/schema"; import { eq } from "drizzle-orm"; const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN }); export const generateAI = inngest.createFunction( { id: "ai-generation-worker", concurrency: 5 }, { event: "app/ai.generate" }, async ({ event, step }) => { const { generationId, prompt } = event.data; try { // 1. Call Replicate (Step ensures retries on network error) const prediction = await step.run("call-replicate", async () => { return await replicate.predictions.create({ version: "model-version-hash", input: { prompt } }); }); // 2. Wait for completion // Replicate usually takes time. We can use waitForEvent if using webhooks, // or simple polling loop with sleep if webhooks aren't set up. // For simplicity, here is a polling pattern using sleep: let finalPrediction = prediction; while (finalPrediction.status !== "succeeded" && finalPrediction.status !== "failed") { await step.sleep("wait-for-gpu", "5s"); finalPrediction = await step.run("check-status", () => replicate.predictions.get(prediction.id) ); } if (finalPrediction.status === "failed") { throw new Error(finalPrediction.error); } // 3. Upload to S3 // Replicate returns a temporary URL. We must persist it. const outputUrl = finalPrediction.output[0]; // Adjust based on model output const s3Url = await step.run("upload-to-s3", async () => { // Fetch image buffer const response = await fetch(outputUrl); const arrayBuffer = await response.arrayBuffer(); const base64 = Buffer.from(arrayBuffer).toString("base64"); // Use existing S3 skill return await uploadFromServer({ file: base64, path: `generations/${generationId}.png`, contentType: "image/png" }); }); // 4. Update DB await step.run("update-db", async () => { await db.update(generations) .set({ status: "completed", url: s3Url }) .where(eq(generations.id, generationId)); }); } catch (error) { // Handle Failure await step.run("mark-failed", async () => { await db.update(generations) .set({ status: "failed" }) .where(eq(generations.id, generationId)); // Optional: Refund credits here }); throw error; // Re-throw to show failure in Inngest dashboard } } );
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 21,033 | 12,870 | -39% | 1 | 1 | 0% | 4,482 | 4,251 | -5% | 0 | 0 | — |
case-02 | fail→pass | 20,545 | 17,958 | -13% | 1 | 1 | 0% | 4,020 | 5,316 | +32% | 0 | 0 | — |
case-03 | fail→pass | 20,504 | 14,579 | -29% | 1 | 1 | 0% | 4,119 | 4,526 | +10% | 0 | 0 | — |
case-04 | pass→pass | 23,649 | 25,102 | +6% | 1 | 1 | 0% | 4,442 | 6,481 | +46% | 0 | 0 | — |
case-05 | pass→pass | 17,568 | 14,843 | -16% | 1 | 1 | 0% | 3,681 | 4,486 | +22% | 0 | 0 | — |
case-06 | pass→pass | 16,229 | 11,542 | -29% | 1 | 1 | 0% | 3,266 | 3,649 | +12% | 0 | 0 | — |
case-07 | fail→pass | 16,071 | 8,738 | -46% | 1 | 1 | 0% | 3,070 | 3,107 | +1% | 0 | 0 | — |
case-08 | fail→pass | 16,712 | 3,318 | -80% | 1 | 1 | 0% | 2,851 | 2,013 | -29% | 0 | 0 | — |
case-09 | fail→pass | 5,905 | 1,844 | -69% | 1 | 1 | 0% | 915 | 1,692 | +85% | 0 | 0 | — |
case-10 | pass→pass | 8,182 | 2,321 | -72% | 1 | 1 | 0% | 1,289 | 1,815 | +41% | 0 | 0 | — |
case-11 | pass→pass | 5,718 | 4,185 | -27% | 1 | 1 | 0% | 810 | 2,079 | +157% | 0 | 0 | — |
case-12 | fail→pass | 13,635 | 9,655 | -29% | 1 | 1 | 0% | 2,356 | 3,022 | +28% | 0 | 0 | — |
case-13 | fail→pass | 9,640 | 2,239 | -77% | 1 | 1 | 0% | 1,433 | 1,822 | +27% | 0 | 0 | — |
case-14 | fail→pass | 12,297 | 4,598 | -63% | 1 | 1 | 0% | 2,174 | 2,238 | +3% | 0 | 0 | — |
case-15 | pass→pass | 9,219 | 3,197 | -65% | 1 | 1 | 0% | 1,587 | 1,983 | +25% | 0 | 0 | — |
case-16 | pass→fail | 16,390 | 14,037 | -14% | 1 | 1 | 0% | 2,816 | 4,015 | +43% | 0 | 0 | — |
case-17 | fail→pass | 4,326 | 2,068 | -52% | 1 | 1 | 0% | 744 | 1,750 | +135% | 0 | 0 | — |
case-18 | fail→pass | 10,069 | 3,186 | -68% | 1 | 1 | 0% | 1,848 | 1,909 | +3% | 0 | 0 | — |
case-19 | fail→pass | 14,277 | 3,763 | -74% | 1 | 1 | 0% | 2,554 | 2,058 | -19% | 0 | 0 | — |
case-20 | pass→pass | 12,968 | 6,672 | -49% | 1 | 1 | 0% | 2,217 | 2,531 | +14% | 0 | 0 | — |
case-21 | fail→fail | 8,111 | 2,813 | -65% | 1 | 1 | 0% | 1,306 | 1,837 | +41% | 0 | 0 | — |
case-22 | fail→pass | 10,447 | 3,005 | -71% | 1 | 1 | 0% | 1,745 | 1,952 | +12% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +55 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.