A batch job that had run untouched for a year failed this week on a single import. The call to load a model, the one that used to read from vertexai.generative_models, now raises ImportError, because Google removed that module on 24 June 2026 after a year on the deprecation notice. Nothing about the model changed. The way you reach it did.
This part is about calling a Gemini model well now that there is one supported way to do it. Gemini is Google’s family of generative models, and the Gemini API is the HTTPS interface you send a prompt to and get a generated response back from. I will start with the plain request, move to streaming so a user sees words instead of a spinner, and finish with function calling, which is how the model asks your code to do something it cannot do on its own. Each step adds one idea to the last.
TL;DR
The Google Gen AI SDK, the package google-genai imported as google.genai, is now the single supported library for calling Gemini on both Vertex AI and the Gemini Developer API. The old vertexai.generative_models modules were removed on 24 June 2026, so pinning google-cloud-aiplatform for generation no longer works. A call is generate_content for a whole answer or generate_content_stream for chunks as they arrive, and streaming changes only when the user sees the first word, not the token bill. Function calling gives the model your function declarations, the model returns a structured request to call one, and you run it and hand back the result. Automatic function calling runs that loop for you, which is convenient and which I turn off in production so I can log, cache, and cap the tool calls. Use mode AUTO for normal traffic and reserve mode ANY for a step that must return structured data.
One SDK now, and the import that broke
For a while there were two ways to call Gemini from Python, and that was the problem. The Vertex AI SDK, google-cloud-aiplatform, carried a set of generative modules under vertexai.generative_models. A separate library, the Google Gen AI SDK, did the same job with a cleaner surface and worked against both Vertex AI and the Gemini Developer API. Google deprecated the older generative modules on 24 June 2025 and removed them on 24 June 2026. If your requirements file still leans on the old import for generation, the fix is not a workaround, it is the migration: install google-genai and call through it.
The new client is small. You create one client object and point it at Vertex AI with your project and region, and every call goes through client.models. The same code talks to the Developer API if you flip one argument, which is the whole reason the library exists. Here is the shape of a first call.
from google import genai
from google.genai import types
client = genai.Client(vertexai=True, project='my-project', location='us-central1')
response = client.models.generate_content(
model='gemini-2.5-flash',
contents='Name three uses for a paperclip.',
config=types.GenerateContentConfig(temperature=0.2, max_output_tokens=256),
)
print(response.text)
Expected output: a short list of three uses printed as plain text. The client authenticates with your environment credentials, sends one request to the regional Vertex AI endpoint, and returns the whole answer at once.
Failure mode: import GenerativeModel from vertexai.generative_models and you now get ImportError, because that module is gone. Forget vertexai=True and the client tries the Developer API path and fails to find your project. The two arguments that matter are the flag and the location.
Anatomy of a generateContent request
Underneath the SDK, that call is the generateContent method on the REST API. Three pieces make up a request. The model id picks which model answers, and gemini-2.5-flash is the fast, cheaper tier while gemini-2.5-pro is the slower one you pay for when a task needs deeper reasoning, a split Part 3 covered. The contents field is the conversation so far, a list of turns each with a role of user or model and one or more parts. The config, a GenerateContentConfig, holds the knobs: a system_instruction that sets the model’s standing orders, a temperature that trades determinism for variety, a max_output_tokens cap, and stop sequences.
For a single question you can pass a plain string as contents and let the SDK wrap it. For a chat you build the list yourself so the model sees the history. The response carries the generated text in response.text for the simple case, and the full structure in response.candidates when you need the finish reason, the safety ratings, or the token counts. Reach for the string form while you are prototyping and the structured form the moment you care about why a response ended.
| Method | What you get back | Reach for it when | Watch out for |
|---|---|---|---|
| generate_content | The whole answer in one response | Batch jobs, parsing, tool steps | User waits for the full generation |
| generate_content_stream | Chunks as the model writes them | Chat and any user-facing UI | Finish reason lands on the last chunk only |
Streaming, and when it earns its keep
A generate_content call returns nothing until the model has finished writing. For a 400 token answer at a typical output rate, that is several seconds of a blank screen. Streaming fixes the feel, not the total. With generate_content_stream the model sends partial results as it produces them, and you print each chunk the moment it lands, so the first words appear in a fraction of a second and the rest scroll in. On the REST side this is the streamGenerateContent method, and you add the alt=sse query parameter to get real server sent events rather than a single JSON array streamed at the end.
for chunk in client.models.generate_content_stream(
model='gemini-2.5-flash',
contents='Explain how a hash map works in 200 words.',
):
print(chunk.text, end='')
Expected output: text appears within a few hundred milliseconds and fills in word by word, instead of the whole paragraph arriving at once after a pause. The total time to the final word is about the same as the non-streaming call.
Two things about streaming trip people who expect the chunks to be smart. The finish reason and the safety ratings only arrive on the last chunk, so you cannot decide a response was blocked or truncated until the stream ends. And a function call does not dribble in as fragments: when the model wants a tool, the whole functionCall shows up in one chunk, and you still accumulate the pieces to reconstruct the full turn. Streaming is a delivery mechanism. It does not change what the model decided, only when you see it.
Worked example
A support chat answers with roughly 400 output tokens. Non-streaming, the user stares at a spinner for about 4.0 seconds, then the whole reply appears. Streaming, the first tokens land at about 0.4 seconds and the reply fills in over the same window, finishing near 4.0 seconds. The total generation time barely moved. The time to the first visible word dropped by roughly ten times.
That is the trade in one line: streaming buys perceived speed, not throughput, and it costs the same tokens either way. For a chat it is worth it every time. For a nightly job that parses the output before a human ever sees it, streaming adds handling code for a latency nobody is waiting on, so I leave it off there.
What function calling actually does
A model cannot look up a live exchange rate or write a row to your database. Function calling, also called tool use, is how it asks your code to do those things. You describe your functions to the model as function declarations, each a name, a plain description, and a JSON schema for its parameters. When a prompt needs one, the model does not run anything. It returns a structured functionCall naming the function and the arguments it wants. Your code executes the function, and you send the result back as a functionResponse so the model can fold it into a final answer.
The single most important sentence here: the model never touches your code. It only produces the request to call a function, and every actual execution happens on your side, under your control, with your logging and your permissions. That is what makes function calling safe to wire into a real system, and it is also why a tool that has side effects, writing data, sending a message, is a decision you own rather than one the model makes.
sequenceDiagram
participant C as Your code
participant M as Model
C->>M: prompt plus function declarations
M-->>C: functionCall get_exchange_rate
C->>C: run the function
C->>M: functionResponse rate 0.92
M-->>C: final answer 100 USD is about 92 EUR
Automatic function calling, and why I turn it off
The SDK offers a shortcut. Pass a plain Python function as a tool instead of a declaration, and automatic function calling, AFC, takes over: the SDK reads the function’s signature, hands the model the schema, and when the model asks for a call, the SDK runs your function and loops back for you until the model produces text. One call in, a finished answer out, tool hops handled invisibly. For a prototype it is a gift.
In production I disable it. AFC hides the very thing I need to see: how many times the model called out, what arguments it used, how long each hop took, and how many tokens the round trips burned. When a tool has side effects, I want a guardrail before it runs, not after. And an AFC loop with no cap can call a tool more times than you expect on a confused prompt. Driving the loop myself is a dozen more lines and it buys logging, caching, a hard cap on hops, and a place to check permissions before anything with side effects fires. Here is that manual loop with AFC switched off.
from google import genai
from google.genai import types
client = genai.Client(vertexai=True, project='my-project', location='us-central1')
# the model never runs this. you do, after it asks.
def get_exchange_rate(base, target):
rates = {'USD-EUR': 0.92, 'USD-GBP': 0.79}
return {'rate': rates.get(base + '-' + target, 0.0)}
rate_fn = types.FunctionDeclaration(
name='get_exchange_rate',
description='Return the exchange rate between two ISO currency codes',
parameters_json_schema={
'type': 'object',
'properties': {
'base': {'type': 'string'},
'target': {'type': 'string'},
},
'required': ['base', 'target'],
},
)
tools = [types.Tool(function_declarations=[rate_fn])]
# AFC off: we drive the loop so we can log, cache and cap it.
cfg = types.GenerateContentConfig(
tools=tools,
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
)
contents = [types.Content(role='user',
parts=[types.Part(text='What is 100 USD in EUR?')])]
resp = client.models.generate_content(
model='gemini-2.5-flash', contents=contents, config=cfg)
call = resp.function_calls[0]
print('model asked for:', call.name, dict(call.args))
result = get_exchange_rate(**call.args) # you run it, with your logging
contents.append(resp.candidates[0].content)
contents.append(types.Content(role='user',
parts=[types.Part.from_function_response(name=call.name, response=result)]))
final = client.models.generate_content(
model='gemini-2.5-flash', contents=contents, config=cfg)
print(final.text)
Expected output: the first print shows the model asking for get_exchange_rate with base USD and target EUR, and the second prints a sentence like 100 USD is about 92 EUR at a rate of 0.92.
Failure mode: leave AFC on, its default when you pass a Python callable as the tool, and the SDK runs get_exchange_rate for you, so resp.function_calls comes back empty and function_calls[0] raises IndexError. Either drive the loop with AFC disabled as above, or rely on AFC and read resp.text, but do not write code that expects both.
Forcing a tool call with mode ANY
By default the model decides for itself whether a prompt needs a tool. You can override that with a tool config, a small setting attached to the request that constrains how the model may use your functions. Its function_calling_config has a mode with three values. AUTO is the default, where the model chooses text or a tool. ANY forces the model to return a function call and disables free text, which is how you guarantee structured output for a step that must produce it. NONE turns tools off for a turn while leaving the declarations in place. With ANY you can also pass allowed_function_names to limit the model to a named subset.
| Mode | What the model may do | Reach for it when |
|---|---|---|
| AUTO | Answer in text or call a tool, its choice | Normal chat and most traffic |
| ANY | Must call a function, no free text | A step that has to return structured data |
| NONE | Text only, tools disabled this turn | You want a plain answer with tools defined |
Stream the answer, own the tool loop
Here is how I wire a Gemini call for a real product. Install google-genai and delete any lingering import from vertexai.generative_models, because that path is gone and the sooner your build fails loudly the better. For anything a person waits on, stream with generate_content_stream so the first words show up fast, and keep plain generate_content for batch work and for the tool steps where you parse the output yourself. Set a system_instruction and a max_output_tokens on every call, not just temperature.
On tools, my rule is to lean on automatic function calling while I am learning the shape of a problem and to turn it off before anything ships. Drive the loop yourself so you can log every hop, cap the number of calls, and gate a side-effecting function behind a permission check. Keep mode on AUTO for normal traffic and reach for ANY only on the single step that must return structured data, narrowed with allowed_function_names, then hand control back to AUTO. That is the difference between a demo that calls a model and a service you can debug at two in the morning.
One thing to do this week: grep your codebase for vertexai.generative_models and for GenerativeModel, and migrate every hit to the Google Gen AI SDK before the next dependency bump breaks a job you had forgotten about. Part 12 builds straight on this call, adding retrieval so the model answers from your own documents with Vertex AI Search and the RAG Engine, and the streaming and function calling you just wired are exactly how those answers get delivered and grounded.
The same calling problem has an AWS shape, where the split is between two request styles rather than two libraries. The mirror of this part is Amazon Bedrock Converse API vs InvokeModel, which solves the same call, stream, and tool problem with different plumbing.
References
- Google Gen AI SDK overview and the deprecation notice
- Introduction to function calling on Vertex AI
- google-genai Python SDK


DrJha