,

Calling Gemini Models with the API, Streaming, and Function Calling (Google Cloud Gen AI Series, Part 11)

The legacy Vertex AI generative SDK was removed in June 2026, so every Gemini call now goes through one library. Here is how to call a model, stream the answer, and wire function calling without letting it run away with your latency.

Google Cloud Gen AI Series · Part 11 of 30

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.

Prerequisites: you have a Google Cloud project with Vertex AI enabled and can run Python with credentials in place, which is the identity you tightened in Part 10. You do not need to have called an LLM in code before. Gemini API, the SDK client, streaming, tool, function declaration, and tool config are each defined here on first use. If regions and endpoints are fuzzy, Part 8 covers them.
The path of one Gemini callyour code talks to the client, the client talks to the endpointYour codea promptGen AI SDK clientgenerate_contentVertex AI endpointgemini-2.5-flash, us-central1response text comes back the same way
One client, one endpoint. The model id and the region decide which model answers, and both live in the call, not in a global setup step.

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.

MethodWhat you get backReach for it whenWatch out for
generate_contentThe whole answer in one responseBatch jobs, parsing, tool stepsUser waits for the full generation
generate_content_streamChunks as the model writes themChat and any user-facing UIFinish reason lands on the last chunk only
In practice: set a system_instruction and a max_output_tokens on every production call, not just temperature. The system instruction is where you pin format and tone so you are not re-sending the same rules in every user prompt, and the token cap is a cheap guard against a runaway answer that quietly triples your bill on one bad input. Two lines of config save more incidents than any prompt trick.

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.

Time to the first visible wordillustrative, a 400 token answer, seconds01.53.04.5seconds4.00.4non-streamingstreamingtotal generation is about 4.0 seconds in both cases
Illustrative, not a benchmark. Streaming collapses the wait before the first word while the total time to finish stays flat, which is why it helps a chat and does nothing for a batch parser.

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
One tool hop is two extra model turns around your code. The model asks for the call, you run it, the model writes the answer from the result.

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.

Disclaimer: a tool that writes data, sends a message, or moves money is a production action the model is requesting, not one it should trigger unattended. Gate any side-effecting function behind your own permission check and, for anything irreversible, a human confirmation, and test the full loop in a non-production project before you wire it to real systems.
What each tool hop addsillustrative wall clock latency by number of tool hops, seconds01.53.04.5seconds1.22.13.00 hops1 hop2 hopseach hop is another model turn plus your function time
Illustrative, not measured. Every tool hop adds a model turn and your function time on top, which is why an uncapped automatic loop can quietly triple a response time you thought was one call.

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.

ModeWhat the model may doReach for it when
AUTOAnswer in text or call a tool, its choiceNormal chat and most traffic
ANYMust call a function, no free textA step that has to return structured data
NONEText only, tools disabled this turnYou want a plain answer with tools defined
Gotcha: ANY forces a call on every turn it governs, so if you leave it on across a multi turn loop the model can never answer in words and you get a tool call when you wanted a reply, or a loop that will not end. Use ANY for the one hop where you need structured output, narrow it with allowed_function_names, then switch back to AUTO so the model can actually respond. Forcing a call is a scalpel, not a default.

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.

Google Cloud Gen AI Series · Part 11 of 30
« Previous: Part 10  |  Guide  |  Next: Part 12 »

References

About The Author


Discover more from Journal of Intelligent Infrastructure – By Dr Pranay Jha

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure - By Dr Pranay Jha

Subscribe now to keep reading and get access to the full archive.

Continue reading