Install @tanstack/ai-lovable. Then call lovableText with a model id such as google/gemini-3.7-flash.
Lovable AI Gateway sits in front of Google and OpenAI models. You use one project key. You do not set up a provider account.
npm install @tanstack/ai-lovableSet LOVABLE_API_KEY. Lovable creates this key for each Cloud project.
export LOVABLE_API_KEY="..."You can also pass the key to a create* factory:
import { createLovableText } from "@tanstack/ai-lovable"
const adapter = createLovableText(
"google/gemini-3.7-flash",
process.env.LOVABLE_API_KEY!,
)The adapter sends Authorization: Bearer, Lovable-API-Key, and X-Lovable-AIG-SDK: tanstack-ai. Calls go to https://ai.gateway.lovable.dev/v1.
The default adapter uses the OpenAI Responses API. Model ids use the google/ or openai/ form.
Server. An endpoint that streams the reply over SSE:
import { chat, toServerSentEventsResponse } from "@tanstack/ai"
import { lovableText } from "@tanstack/ai-lovable"
export async function POST(request: Request) {
const { messages } = await request.json()
const stream = chat({
adapter: lovableText("google/gemini-3.7-flash"),
messages,
})
return toServerSentEventsResponse(stream)
}Client. The same useChat hook as every other provider:
import { useState } from "react"
import { fetchServerSentEvents, useChat } from "@tanstack/ai-react"
export function Chat() {
const [input, setInput] = useState("")
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents("/api/chat"),
})
return (
<div>
{messages.map((message) => (
<div key={message.id}>
<strong>{message.role}</strong>
{message.parts.map((part, index) =>
part.type === "text" ? <p key={index}>{part.content}</p> : null,
)}
</div>
))}
<form
onSubmit={(event) => {
event.preventDefault()
if (!input.trim() || isLoading) return
sendMessage(input)
setInput("")
}}
>
<input value={input} onChange={(event) => setInput(event.target.value)} />
<button type="submit" disabled={isLoading}>
Send
</button>
</form>
</div>
)
}Pass { api: "chat" } when the model must talk to Chat Completions. The default is Responses.
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" }],
})api: "responses" is the same as the default. api: "chat-completions" is the same as api: "chat".
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.",
})Generate a new image, or pass image parts to edit one. OpenAI image models also accept a mask part (metadata.role === "mask").
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",
})Video jobs are async. Create a job, poll status, then fetch the MP4 URL. Clips last 4, 6, or 8 seconds. 1080p and 4K clips are always 8 seconds. 4K works only on google/veo-3.1-fast and google/veo-3.1.
import { generateVideo } from "@tanstack/ai"
import { lovableVideo } from "@tanstack/ai-lovable"
const { jobId } = await generateVideo({
adapter: lovableVideo("google/veo-3.1-lite"),
prompt: "a red guitar on a wooden bench, slow camera push-in",
duration: 4,
size: "1280x720",
})Then poll getVideoJobStatus with that jobId. When the job is complete, the result includes the MP4 URL.
Pass one image part in prompt to animate a still frame.
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)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.",
voice: "nova",
})import { generateTranscription } from "@tanstack/ai"
import { lovableTranscription } from "@tanstack/ai-lovable"
const audio = await fetch("/voice-note.mp3").then((response) => response.blob())
const result = await generateTranscription({
adapter: lovableTranscription("openai/gpt-4o-mini-transcribe"),
audio,
language: "en",
})
console.log(result.text)Users can paste a Lovable project key in the browser. Import lovableByok from @tanstack/ai-lovable/byok, not from the package main entry.
import { createLovableText } from "@tanstack/ai-lovable"
import { lovableByok } from "@tanstack/ai-lovable/byok"
import { byokMissing, getByokKey } from "@tanstack/ai/byok/server"
export async function POST(request: Request) {
const apiKey = getByokKey(request, lovableByok)
if (!apiKey) return byokMissing(lovableByok)
const adapter = createLovableText("google/gemini-3.7-flash", apiKey)
// ...
}See Bring Your Own Key for the client store and a save UI.
Pass any model id the gateway accepts. Curated ids get type metadata.
Chat:
Image, video, embeddings, and speech:
The curated lists are LOVABLE_CHAT_MODELS, LOVABLE_IMAGE_MODELS, LOVABLE_VIDEO_MODELS, LOVABLE_EMBEDDING_MODELS, LOVABLE_TTS_MODELS, and LOVABLE_TRANSCRIPTION_MODELS.
See Lovable AI features for models, credits, and rate limits.