Google's old JavaScript SDK now lives in a repository named deprecated-generative-ai-js, and its README says all support, including bug fixes, ended on November 30, 2025. The new SDK covers the Gemini Developer API and Google Cloud from one package, and the Gemini docs only show it. Most changes are mechanical, but two of them fail at runtime rather than at compile time in plain JavaScript: calling text() on what is now a string, and iterating result.stream, which no longer exists. This guide follows Google's migration guide and the js-genai README, linked at the end.
What changed
new GoogleGenerativeAI(key)becomesnew GoogleGenAI({ apiKey }).genAI.getGenerativeModel({ model })is gone. Passmodelto each call onai.models.model.generateContent(prompt)becomesai.models.generateContent({ model, contents }), and the result is the response itself, not{ response }.response.text()is now a property:response.text.- Streams are iterated directly:
for await (const chunk of stream)instead ofresult.stream. generationConfigandsafetySettingsmove into a per-requestconfigobject.GoogleAIFileManagerandGoogleAICacheManagerfrom@google/generative-ai/serverare replaced byai.filesandai.caches.
Migration steps
Swap the package
npm uninstall @google/generative-ai npm install @google/genaiThe README lists Node.js 20 or later as a prerequisite and warns that SDK version 3.0.0 will require Node.js 22. It also says not to ship API keys in browser code; keep calls on the server.
Create a client and call ai.models
Before
import { GoogleGenerativeAI } from "@google/generative-ai"; const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); const model = genAI.getGenerativeModel({ model: "gemini-flash-latest" }); const result = await model.generateContent("Tell me a story in 300 words"); console.log(result.response.text());After
import { GoogleGenAI } from "@google/genai"; const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); const response = await ai.models.generateContent({ model: "gemini-flash-latest", contents: "Tell me a story in 300 words", }); console.log(response.text);Google's code generation notes also list the type renames:
GenerateContentResultbecomesGenerateContentResponseandGenerateContentRequestbecomesGenerateContentParameters.Move generationConfig into config
Before
const model = genAI.getGenerativeModel({ model: "gemini-flash-latest", generationConfig: { stopSequences: ["x"], maxOutputTokens: 20, temperature: 1.0 }, }); const result = await model.generateContent("Tell me a story about a magic backpack.");After
const response = await ai.models.generateContent({ model: "gemini-flash-latest", contents: "Tell me a story about a magic backpack.", config: { stopSequences: ["x"], maxOutputTokens: 20, temperature: 1.0, systemInstruction: "You are a storyteller for young children", }, });Settings that used to live on the model object are now per request.
safetySettingsgoes in the sameconfig, with string categories such as"HARM_CATEGORY_HARASSMENT". If several call sites shared one configured model, keep a shared config object and spread it into each call.Iterate streams directly
Before
const result = await model.generateContentStream(prompt); for await (const chunk of result.stream) { process.stdout.write(chunk.text()); }After
const stream = await ai.models.generateContentStream({ model: "gemini-flash-latest", contents: prompt, }); for await (const chunk of stream) { process.stdout.write(chunk.text ?? ""); }Create chats from ai.chats
Before
const chat = model.startChat({ history: [ { role: "user", parts: [{ text: "Hello" }] }, { role: "model", parts: [{ text: "Great to meet you. What would you like to know?" }] }, ], }); const result = await chat.sendMessage("I have 2 dogs in my house."); console.log(result.response.text());After
const chat = ai.chats.create({ model: "gemini-flash-latest", history: [ { role: "user", parts: [{ text: "Hello" }] }, { role: "model", parts: [{ text: "Great to meet you. What would you like to know?" }] }, ], }); const response = await chat.sendMessage({ message: "I have 2 dogs in my house." }); console.log(response.text);sendMessagetakes an object with amessagefield instead of a bare string. For streaming chats usechat.sendMessageStream({ message })and iterate the result withfor await.Replace the server helpers and other model methods
new GoogleAIFileManager(key).uploadFile(...)toai.files.upload({ file })new GoogleAICacheManager(key).create(...)toai.caches.create(...)model.countTokens(prompt)toai.models.countTokens({ model, contents: prompt });totalTokensis still on the resultmodel.embedContent(text)toai.models.embedContent({ model: "gemini-embedding-001", contents: text }), which returnsembeddings(plural)result.response.usageMetadatatoresponse.usageMetadata
Catch ApiError
import { ApiError } from "@google/genai"; try { await ai.models.generateContent({ model: "non-existent-model", contents: "Hi" }); } catch (e) { if (e instanceof ApiError) console.error(e.status, e.message); else throw e; }ApiErrorextendsErrorand addsstatus, the HTTP code.
How to find every affected call
Use ast-grep to match the syntax tree rather than text, so calls split across lines are found. $GENAI and $MODEL match whatever you named the instances:
sg run --pattern 'new GoogleGenerativeAI($$$ARGS)' --lang ts
sg run --pattern '$GENAI.getGenerativeModel($$$ARGS)' --lang ts
sg run --pattern '$RESULT.response.text()' --lang ts
sg run --pattern '$RESULT.stream' --lang ts
sg run --pattern '$MODEL.startChat($$$ARGS)' --lang ts
sg run --pattern 'new GoogleAIFileManager($$$ARGS)' --lang tsRepeat with --lang tsx and --lang js. $RESULT.stream also matches unrelated objects with a stream property, so review each hit. chunk.text() inside old stream loops is a separate search: sg run --pattern '$CHUNK.text()' --lang ts.
Testing the change
- Run
tsc --noEmit. Leftover.text()calls,result.responsereads and old type names show up as type errors in TypeScript files. - Update mocks. A stub of
getGenerativeModelthat returns{ response: { text: () => "..." } }keeps passing while production breaks; return the new shape withtextas a string. - Run one streamed request and one chat in staging, since both changed how you iterate and how you send a message.
- Trigger an error with a bad model name and confirm your handler sees an
ApiErrorwith a numericstatus.
Automating it
Google does not publish a codemod. It publishes codegen_instructions.md, a list of incorrect and correct patterns meant to be pasted into an AI coding tool so it writes new-SDK code. That helps a model avoid old calls, but nothing checks that the result builds.
Versyka finds the call sites deterministically with ast-grep, rewrites them together with the code that depends on them (the .text() reads, stream loops, mocks), then runs your repository's install, typecheck, build and tests in a disposable sandbox. A pull request is opened only when those pass.
Was this guide helpful?
Check your own repository
Paste your package.json, requirements.txt or pyproject.toml into the free upgrade check to see which API SDKs are behind, with no account. Or let Versyka run this migration on one repository and open a pull request that has already passed your tests.