TanStack
Adapters

Lovable AI Gateway

The Lovable adapter calls Google and OpenAI models with one project key. It covers chat, images, video, embeddings, speech, and transcription.

1. Install

shell
npm i @tanstack/ai-lovable

2. Set the API key

Lovable creates a LOVABLE_API_KEY for each Cloud project. Put it in the environment of your server:

shell
LOVABLE_API_KEY=your-lovable-key

lovableText and the other short factories read LOVABLE_API_KEY. To pass the key in code, use the create* factory of the same adapter:

ts
import { createLovableText } from "@tanstack/ai-lovable";

const adapter = createLovableText("google/gemini-3.7-flash", process.env.LOVABLE_API_KEY!);

3. Stream a chat from the server

Model ids use the google/ or openai/ form:

ts
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from "@tanstack/ai";
import { lovableText } from "@tanstack/ai-lovable";

export async function POST(request: Request) {
  const { messages, threadId, runId } = await chatParamsFromRequest(request);

  const stream = chat({
    adapter: lovableText("google/gemini-3.7-flash"),
    messages,
    threadId,
    runId,
  });

  return toServerSentEventsResponse(stream);
}

4. Render the chat

The client is the same for every provider. Point useChat at your route:

tsx
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";

export function Chat() {
  const [input, setInput] = useState("");
  const { messages, sendMessage } = useChat({
    connection: fetchServerSentEvents("/api/chat"),
    threadId: "support-chat",
  });

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        sendMessage(input);
        setInput("");
      }}
    >
      {messages.map((message) => (
        <p key={message.id}>
          {message.parts
            .map((part) => (part.type === "text" ? part.content : ""))
            .join("")}
        </p>
      ))}
      <input value={input} onChange={(event) => setInput(event.target.value)} />
    </form>
  );
}

For Vue, Solid, Svelte, Angular, and the other frameworks, see Quick Start.

You now have a streaming chat on Lovable. To use an OpenAI model, change the id, for example to openai/gpt-5.6-sol.

Images

Make a new image from a prompt:

ts
import { generateImage } from "@tanstack/ai";
import { lovableImage } from "@tanstack/ai-lovable";

const result = await generateImage({
  adapter: lovableImage("openai/gpt-image-2"),
  prompt: "a red guitar on a wooden bench",
  threadId: "thread-1",
});

To edit an image, pass image parts. OpenAI image models also take a mask part (metadata.role === "mask"). See Image Generation.

Video

Note

Video generation is an experimental feature.

Video runs as a job. generateVideo() starts the job and returns a jobId. Poll the job until it has an MP4 URL:

ts
import { generateVideo, getVideoJobStatus } from "@tanstack/ai";
import { lovableVideo } from "@tanstack/ai-lovable";

const adapter = lovableVideo("google/veo-3.1-lite");

const { jobId } = await generateVideo({
  adapter,
  prompt: "a red guitar on a wooden bench, slow camera push-in",
  threadId: "thread-1",
  duration: "4s",
  size: "1280x720",
});

let status = await getVideoJobStatus({ adapter, jobId });
while (status.status === "pending" || status.status === "processing") {
  await new Promise((resolve) => setTimeout(resolve, 5000));
  status = await getVideoJobStatus({ adapter, jobId });
}

console.log(status.status === "completed" ? status.url : status.error);
  • A clip is 4, 6, or 8 seconds. You can write 4, "4", or "4s". The same forms work for 6 and 8.
  • 1080p and 4K clips are always 8 seconds.
  • 4K works only on google/veo-3.1-fast and google/veo-3.1.
  • To animate a still frame, pass one image part in prompt.

Embeddings

ts
import { embed } from "@tanstack/ai";
import { lovableEmbedding } from "@tanstack/ai-lovable";

const result = await embed({
  adapter: lovableEmbedding("google/gemini-embedding-2"),
  input: "a red guitar",
});

console.log(result.embeddings[0]?.vector);

Speech

ts
import { generateSpeech } from "@tanstack/ai";
import { lovableSpeech } from "@tanstack/ai-lovable";

const result = await generateSpeech({
  adapter: lovableSpeech("openai/gpt-4o-mini-tts"),
  text: "Welcome to the guitar shop.",
  threadId: "thread-1",
  voice: "nova",
});

See Text-to-Speech.

Transcription

audio takes a File, a Blob, an ArrayBuffer, a base64 string, or a data URL:

ts
import { generateTranscription } from "@tanstack/ai";
import { lovableTranscription } from "@tanstack/ai-lovable";

const audio = await fetch("https://example.com/voice-note.mp3").then((response) =>
  response.blob(),
);

const result = await generateTranscription({
  adapter: lovableTranscription("openai/gpt-4o-mini-transcribe"),
  audio,
  language: "en",
  threadId: "thread-1",
});

console.log(result.text);

See Transcription.

Summarization

ts
import { summarize } from "@tanstack/ai";
import { lovableSummarize } from "@tanstack/ai-lovable";

const result = await summarize({
  adapter: lovableSummarize("google/gemini-3.7-flash"),
  text: "The Fender Stratocaster is a versatile electric guitar.",
  threadId: "thread-1",
});

Models and options

Reasoning

Set reasoning on chat(). The types take the levels of each model:

ts
import { chat } from "@tanstack/ai";
import { lovableText } from "@tanstack/ai-lovable";

const stream = chat({
  adapter: lovableText("openai/gpt-5.6-sol"),
  messages: [{ role: "user", content: "Plan a database migration." }],
  threadId: "thread-1",
  reasoning: "xhigh",
});
  • Responses (the default) sends reasoning.effort. Chat Completions sends reasoning_effort.
  • Neither takes budgetTokens.
  • The OpenAI and Gemini models take the same levels as on OpenAI and Gemini.

Models

Pass a model id from the curated lists. TypeScript rejects an id that is not on a list.

ActivityDefault modelList
Chatgoogle/gemini-3.7-flashLOVABLE_CHAT_MODELS
Imagesopenai/gpt-image-2LOVABLE_IMAGE_MODELS
Videogoogle/veo-3.1-liteLOVABLE_VIDEO_MODELS
Embeddingsgoogle/gemini-embedding-2LOVABLE_EMBEDDING_MODELS
Speechopenai/gpt-4o-mini-ttsLOVABLE_TTS_MODELS
Transcriptionopenai/gpt-4o-mini-transcribeLOVABLE_TRANSCRIPTION_MODELS

For chat, openai/gpt-5.5 is a fast OpenAI choice.

Advanced

Chat Completions

The default text adapter uses the OpenAI Responses API. If a model must use Chat Completions, pass { api: "chat" }:

ts
import { chat } from "@tanstack/ai";
import { lovableText } from "@tanstack/ai-lovable";

const stream = chat({
  adapter: lovableText("openai/gpt-5.5", { api: "chat" }),
  messages: [{ role: "user", content: "Hello" }],
  threadId: "thread-1",
});
  • api: "responses" is the same as the default.
  • api: "chat-completions" is the same as api: "chat".

Need a header on each request, or a log of each model call? The chat adapters on this page support wrapFetch.

Bring Your Own Key

Users can paste a Lovable project key in the browser. Import lovableByok from @tanstack/ai-lovable/byok. The package main entry does not export it.

ts
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from "@tanstack/ai";
import { byokMissing, getByokKey } from "@tanstack/ai/byok/server";
import { createLovableText } from "@tanstack/ai-lovable";
import { lovableByok } from "@tanstack/ai-lovable/byok";

export async function POST(request: Request) {
  const apiKey = getByokKey(request, lovableByok);
  if (!apiKey) return byokMissing(lovableByok);

  const { messages, threadId, runId } = await chatParamsFromRequest(request);

  const stream = chat({
    adapter: createLovableText("google/gemini-3.7-flash", apiKey),
    messages,
    threadId,
    runId,
  });

  return toServerSentEventsResponse(stream);
}

See Bring Your Own Key for the client store and a save UI.

Requests

  • Calls go to https://ai.gateway.lovable.dev/v1.
  • The adapter sends Authorization: Bearer, Lovable-API-Key, and X-Lovable-AIG-SDK: tanstack-ai.

Errors

  • 429 Too Many Requests: the workspace hit its limit of requests per minute.
  • 402 Payment Required: the workspace has no credits left.

See Lovable AI features for models, credits, and rate limits.