Gemini API in Python: requests via the OpenAI SDK and OpenAI format
A working Gemini API example in Python with no extra library: change base_url in the OpenAI SDK, add streaming and JSON, and compare with google-genai.
5 min · · development · API keys
If you are looking for how to call the Gemini API from Python, you have two options: Google's native library, or the familiar OpenAI SDK pointed at a different address. This article covers the second option with working code: a client, a plain request, streaming, JSON output, error handling and a cost estimate. At the end we compare it with the native google-genai and say when each one fits.
All examples go through the Zukko gateway: one fetch_... key, the OpenAI format and the gemini-3.8-flash model. Prices are as of October 18, 2026.
Why call Gemini through the OpenAI SDK
The main benefit is that your code is not tied to one vendor. The OpenAI(...) client only knows three things: an address, a key and a model name. To move from Gemini to GPT or Claude you change the model string. You do not rewrite response parsing, streaming or error handling.
That helps in three situations:
- the project already uses
openaiand you want to add a cheap model for bulk tasks; - you are comparing several models on your own data and do not want to write an adapter for each;
- a tool (LangChain, LlamaIndex, your own service) only speaks the OpenAI-compatible API.
Google itself supports this mode: the Gemini API documentation has a section on OpenAI library compatibility where you change three lines (key, base URL and model). The same page says that if you do not use OpenAI libraries, calling the Gemini API directly is simpler. We come back to that in the comparison section.
Installation and the first request
pip install openaiCreate a key on the API keys page and keep it in an environment variable rather than in code:
export OPENAI_BASE_URL="https://api.zukko.pro/v1"
export OPENAI_API_KEY="fetch_your_key"A minimal request:
from openai import OpenAI
client = OpenAI() # reads OPENAI_BASE_URL and OPENAI_API_KEY
MODEL = "gemini-3.8-flash"
resp = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "Answer briefly and to the point."},
{"role": "user", "content": "How is a list different from a tuple in Python?"},
],
)
print(resp.choices[0].message.content)
print(resp.usage)If you prefer to set the address explicitly instead of using environment variables:
client = OpenAI(base_url="https://api.zukko.pro/v1", api_key="fetch_your_key")The address ends with /v1 because the OpenAI SDK only appends /chat/completions itself. For more on the client see the Python section of the API reference, and the chat completions page next to it covers the endpoint itself.
Streaming
For interfaces and chat bots it is convenient to receive the answer in pieces:
stream = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": "Tell me about generators in Python"}],
stream=True,
)
for chunk in stream:
if chunk.choices:
piece = chunk.choices[0].delta.content
if piece:
print(piece, end="", flush=True)
elif chunk.usage:
print(f"\n[in: {chunk.usage.prompt_tokens}, out: {chunk.usage.completion_tokens}]")The if chunk.choices: check is required. At the end of the stream the server sends a final chunk with usage and an empty choices list, and accessing chunk.choices[0] without the check raises IndexError.
JSON instead of free text
If a program has to parse the answer, ask for JSON explicitly and still validate the result:
import json
resp = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "system",
"content": "Return only a JSON object with keys name, date, participants (array of strings).",
},
{"role": "user", "content": "Anna and Boris are going to an AI conference on Friday."},
],
response_format={"type": "json_object"},
)
try:
data = json.loads(resp.choices[0].message.content)
except json.JSONDecodeError:
data = None # retry the request or send it to manual review
print(data)The response_format parameter belongs to the OpenAI format. Google's compatibility documentation shows schema-based structured output examples, but how exactly the parameter reaches the model through a gateway is worth testing on your own request. If you get a 400 error, drop response_format and keep only the prompt instruction plus json.loads: for simple schemas that is usually enough.
Errors and retries
from openai import OpenAI, APIStatusError, APIConnectionError
try:
resp = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": "Hello!"}],
)
except APIStatusError as e:
if e.status_code == 401:
print("Invalid key")
elif e.status_code == 402:
print("Insufficient balance")
elif e.status_code == 404:
print("Model is not in the catalog, check GET /v1/models")
elif e.status_code in (429, 503):
print("Key limit or provider unavailable, retry later")
else:
raise
except APIConnectionError:
print("Network unavailable")Retrying makes sense for 429 and 503, with a growing pause. Errors 401, 402 and 403 are not fixed by retries. If a model is switched off, the server returns 503 with the code model_disabled: pick another model from GET /v1/models. The model catalog can always be fetched with client.models.list().
What it costs
Zukko prices as of October 18, 2026 for gemini-3.8-flash: $0.125 per 1M input tokens and $0.6 per 1M output tokens. Context is 1M tokens, and only text is accepted as input.
An example with illustrative volumes: 1000 requests of 2000 input and 500 output tokens each.
- input: 1000 × 2000 = 2M tokens, 2 × 0.125 = $0.25;
- output: 1000 × 500 = 0.5M tokens, 0.5 × 0.6 = $0.30;
- total: 0.25 + 0.30 = $0.55.
At the official prices from our table (input $0.75, output $3.75) the same volume would cost 2 × 0.75 + 0.5 × 3.75 = $3.375. The exact amount for each request is in the X-Zukko-Cost-Micros header (in micro-dollars) via with_raw_response:
raw = client.chat.completions.with_raw_response.create(
model=MODEL,
messages=[{"role": "user", "content": "Hello!"}],
)
print(raw.headers.get("x-zukko-cost-micros"))
resp = raw.parse()The header is only present on regular (non-streaming) responses. The current list of models and prices is GET https://api.zukko.pro/v1/models or the Models page in the dashboard. If the model reasons before answering, those tokens count toward output, so on hard prompts look at usage rather than the length of the visible answer.
Native google-genai or the OpenAI SDK
| Criterion | OpenAI SDK via gateway | Native google-genai |
|---|---|---|
| Install | pip install openai | pip install google-genai |
| Request shape | messages, chat.completions | input, interactions.create |
| Switching vendors | change the model string | rewrite the code |
| Gemini specifics | via compatibility-layer parameters | full Google feature set |
| Key | gateway fetch_... key | Google AI Studio key |
The native client looks like this (per Google's current quickstart, the key is read from GEMINI_API_KEY):
from google import genai
client = genai.Client()
interaction = client.interactions.create(
model="gemini-3.8-flash",
input="How is a list different from a tuple in Python?",
)
print(interaction.output_text)Choose the native library if you need features missing from the compatibility layer, such as full thinking controls and anything Google ships before others. Choose the OpenAI SDK if portable code, one format for many models and a shared balance matter more. One caveat on parameters: Google's troubleshooting page says newer models in the line (3.6 and later) ignore custom temperature and top_p values, so do not count on them as a tuning knob.
Try it on your own data: create a key in the dashboard, drop it into the example above and compare gemini-3.8-flash answers with gpt-6.1-sol or deepseek-v4.1-flash by changing one line. For choosing between models by price and strength index see the model strength index article, and for Gemini as a standalone model see Gemini 3.8 Flash via API.
FAQ
Do I need a separate Google key to call Gemini through the OpenAI SDK?
Not through the Zukko gateway: a fetch_... key is enough. Going directly to Google requires a key from Google AI Studio, and availability depends on your region.
Which base_url should I use?
https://api.zukko.pro/v1, with the /v1 suffix. The Anthropic SDK uses a different address (without /v1), but that does not apply to Gemini.
Do streaming and function calling work?
Streaming works in the OpenAI format, see the example above. For function calling and other compatibility-layer features, follow Google's documentation and test the behavior on your own task.
How do I find the exact model name?
Send GET https://api.zukko.pro/v1/models with your key or open the Models page in the dashboard. An unknown name returns a 404 model_not_found error.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Anthropic SDK for Python: base_url, key and a first request via a gatewayThe official anthropic package for Python just needs a different address. We cover base_url, the key, streaming, token counting, caching and errors.
- Claude Code security: a checklist before running the agent on a projectThe agent reads files and runs commands, so security settings belong before the first launch. A checklist: permissions, secrets, sandbox, a separate key with a limit.