Use Gemini with the OpenAI SDK in JavaScript (and the Agents SDK too)
Gemini speaks OpenAI's chat completions format, which means you can keep your existing OpenAI client and just swap the base URL. Here's the exact setup for plain chat completions, the OpenAI Agents SDK, and the three errors that will eat your afternoon if you don't know to look for them.

I had an OpenAI Agents SDK prototype working on a Tuesday and wanted to try the same workflow against Gemini the next morning. By Wednesday afternoon I had rewritten most of the code twice, mostly because the documentation I found assumed I was starting from scratch and the snippets I did find mixed chat.completions and the new Responses API without warning. This post is the version of that Wednesday I wish someone had handed me at 9am.
The short version: Gemini exposes an OpenAI-compatible endpoint at generativelanguage.googleapis.com. Point the OpenAI SDK at it with a custom baseURL, give it a Gemini API key, and the chat completions API works exactly like it does against OpenAI. The Agents SDK takes a little more wiring because it has a ModelProvider abstraction, but it's a ten-line setup once you know which functions to call.
The long version follows.
What "OpenAI compatible" actually means here
Google runs an OpenAI-shaped endpoint at https://generativelanguage.googleapis.com/v1beta/openai/. It accepts the same chat.completions.create({...}) request body the OpenAI SDK already builds, returns the same response shape, and streams in the same Server-Sent Events format. You don't need a custom client, a shim package, or any translation layer. You just construct the SDK with a different baseURL.
A few things this gets you for free, and a few things it deliberately does not:
- ✅ Chat completions, both regular and streaming.
- ✅ Function calling / tool use, with the same
tools: [...]array shape. - ✅ Structured outputs via
response_format(the JSON Schema variant — Gemini maps it onto its own constrained-decoding backend). - ✅ System messages, multi-turn history, temperature,
max_tokens. - ❌ The newer Responses API. Gemini does not implement
/v1/responsesas of mid-2026. If the SDK silently upgrades your request to Responses, you'll get a 404 with no obvious cause. - ❌ The Assistants API. This is an OpenAI-only thing and not relevant to Gemini.
- ❌ OpenAI-specific tools like the file search or code interpreter built-ins. You bring your own.
That last bullet is the one that matters for the Agents SDK section below. The Agents SDK's tool() helper works fine — it's just a schema for declaring a function — but you have to actually run the function yourself. There's no remote execution.
Prerequisites
You need three things:
- Node.js 18 or higher. The OpenAI SDK dropped 16 a while back and the Agents SDK has hard requirements on
globalThis.cryptoand other 18+ features. - A Gemini API key from Google AI Studio. It's free to create, free for the lower-rate limits, and the key is shown once. Copy it somewhere safe.
- A fresh Node project. I'll assume you have one. If not,
npm init -yand you're set.
Install the four packages this post uses:
npm install openai @openai/agents dotenv zodzod is optional but the Agents SDK's tool() helper is much nicer with a schema than with a hand-written JSON Schema object, so I'd install it from the start.
1. Project setup
Create a .env file in your project root. Keep .env in your .gitignore — it almost always is by default, but worth saying out loud.
BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai/
API_KEY=your-gemini-api-key
MODEL_NAME=gemini-2.5-flashThree variables, one job each:
BASE_URLis the OpenAI-compatible Gemini endpoint. The trailing slash matters — the OpenAI SDK concatenates paths, and an inconsistent slash can produce URLs like.../openai/chat/completionswith a double slash or.../openaichat/completionswithout one. Both fail. Use the trailing slash.API_KEYis your Gemini key from AI Studio. The SDK calls it theapiKeyfield, but it's the Gemini key, not an OpenAI key.MODEL_NAMEis whichever Gemini model you want.gemini-2.5-flashis the cheap, fast default I'd reach for first.gemini-2.5-prois the slower, smarter one. The model name has to match exactly —gemini-2.5with no-flashsuffix will 404.
2. Plain chat completions (the sanity check)
Before wiring up the Agents SDK, prove the base setup works. This is the same script I'd run for any new provider integration — small, end-to-end, and if it fails here you know the problem is in the env vars, not in agent plumbing.
import OpenAI from "openai";
import "dotenv/config";
if (!process.env.BASE_URL || !process.env.API_KEY || !process.env.MODEL_NAME) {
throw new Error("Please set BASE_URL, API_KEY, and MODEL_NAME in .env");
}
const openai = new OpenAI({
apiKey: process.env.API_KEY,
baseURL: process.env.BASE_URL,
});
async function simpleChat() {
const res = await openai.chat.completions.create({
model: process.env.MODEL_NAME,
messages: [
{ role: "user", content: "Write a short poem about JavaScript" },
],
});
console.log(res.choices[0].message.content);
}
simpleChat();Run it:
node simple-chat.jsIf you get a poem back, your key, endpoint, and model name are all valid. If you get an error, the error message will point at one of those three things, and you can stop reading this post until that works. The Agents SDK on top of a broken plain-client setup is the slowest possible debugging experience — every failure looks like an agent orchestration problem when it's actually just your model name being wrong.
3. Wiring Gemini into the OpenAI Agents SDK
The OpenAI Agents SDK is a thin orchestration layer on top of the OpenAI SDK. It adds a Runner, an Agent class, a tool() helper, and a handoff mechanism. Internally it still calls chat.completions.create() — so if the plain client works, the Agents SDK will work once you tell it to use your custom client.
There are four things to configure, in this order:
- Build a custom
OpenAIclient pointed at Gemini. - Wrap it in a
ModelProvider. - Set it as the default so the SDK doesn't try to construct its own.
- Pin the API to
chat_completionsso it never tries the Responses endpoint.
Here's a working example with a single tool. The shape is what I'd actually ship — small, runnable, with one tool that exercises the function-calling path end to end.
import {
Agent,
Runner,
setTracingDisabled,
tool,
OpenAIProvider,
setDefaultOpenAIClient,
setOpenAIAPI,
} from "@openai/agents";
import OpenAI from "openai";
import "dotenv/config";
import { z } from "zod";
if (!process.env.BASE_URL || !process.env.API_KEY || !process.env.MODEL_NAME) {
throw new Error("Missing BASE_URL, API_KEY, or MODEL_NAME in .env");
}
// 1. Custom OpenAI client pointed at Gemini
const openaiClient = new OpenAI({
apiKey: process.env.API_KEY,
baseURL: process.env.BASE_URL,
});
// 2. Wrap in a provider
const modelProvider = new OpenAIProvider({ openAIClient: openaiClient });
// 3. Set as default — without this, the Runner tries to build its own
// client from OPENAI_API_KEY, which you don't have.
setDefaultOpenAIClient(openaiClient);
// 4. Pin the API surface. Gemini does not implement the Responses API.
setOpenAIAPI("chat_completions");
// Tracing sends requests to OpenAI's tracing backend. We don't have an
// OpenAI key, so disable it. Re-enable if you ever switch back.
setTracingDisabled(true);
// Example tool — schema via zod, body runs locally.
const getWeather = tool({
name: "get_weather",
description: "Get the current weather for a city.",
parameters: z.object({
city: z.string().describe("The city to get weather for"),
}),
async execute({ city }) {
console.log(`[debug] getting weather for ${city}`);
return `The weather in ${city} is sunny, 72°F, wind 5 mph.`;
},
});
async function main() {
const agent = new Agent({
name: "Assistant",
instructions:
"You only respond in short sentences. Always mention temperature and wind speed.",
model: process.env.MODEL_NAME,
tools: [getWeather],
});
const runner = new Runner({ modelProvider });
const result = await runner.run(agent, "What's the weather in Tokyo?");
console.log(result.finalOutput);
}
main();A few non-obvious things in there, in the order you'll hit them when reading the code:
setDefaultOpenAIClientis not optional. If you skip it, the Runner readsOPENAI_API_KEYfrom the environment, doesn't find one, and errors with a message that says "missing API key" instead of "I don't know which client to use." Very confusing. Calling it once at module top-level fixes both problems.setOpenAIAPI("chat_completions")is the Responses-API guard. Without it, the SDK may try to call/v1/responsesand Gemini returns a 404. The flag pins everything to chat completions.setTracingDisabled(true)is for this specific setup. The Agents SDK ships with an OpenAI-hosted tracing pipeline. Without a real OpenAI API key, the first tracing call will fail and the whole agent run aborts. Disable it. If you ever migrate this to OpenAI directly, remove that line.- The
modelfield on theAgentis the Gemini model name, not "gpt-4o". The string goes straight to the chat completions request.gemini-2.5-flashworks.gpt-4owould 404.
4. Streaming, if you want it
Both the plain client and the Agents SDK support streaming against Gemini without any extra configuration. The OpenAI SDK exposes it as stream: true on chat.completions.create, and the Agents SDK surfaces it on runner.run(agent, input, { stream: true }). The wire format is the same Server-Sent Events the OpenAI SDK already knows how to parse.
The one thing to be aware of: if you're using the Responses API accidentally (because you forgot setOpenAIAPI("chat_completions")), streaming gives a less useful error than non-streaming. You'll get a vague 404 from the SSE handler instead of a clear "endpoint not found" from the JSON error body. If streaming hangs and then errors with no message, the API pin is the first thing I'd check.
5. Three errors that ate my Wednesday
These are the things that actually broke, in the order they broke, with the fix that worked.
"Model not found" with a model name that definitely exists
The model name is case-sensitive and version-pinned. Gemini-2.5-Flash 404s. gemini-2.5-flash-latest 404s. gemini-2.5-flash works. Pick the exact string from the Gemini models list and copy it as-is.
401 with a key you just created
AI Studio shows the key once and gives you a "copy" button. If you copied it via the AI Studio UI's share dialog instead of the key-creation modal, you may have copied a project ID or a placeholder. Re-generate the key from the API keys page, copy it from the success modal (the one that says "you'll only see this once"), and paste it into .env. Restart the process — dotenv only reads on startup.
"endpoint not found" with setOpenAIAPI("chat_completions") already set
The most embarrassing one. I had the pin set correctly, the model name was right, the key worked in simpleChat.js, and the Agents SDK still 404'd. The cause: I had a stale OPENAI_API_KEY in my shell environment from an earlier project. The Agents SDK reads OPENAI_API_KEY before it consults setDefaultOpenAIClient in some code paths, and when that env var is set to garbage, the request goes to OpenAI's endpoint with a Gemini key and OpenAI returns 404. Unset it:
unset OPENAI_API_KEYSame family of problem as the ANTHROPIC_API_KEY issue from the Claude Code post: any leftover env var from a previous integration will silently win over the explicit setup you wrote in code.
6. A small but real cost: tool schemas
Gemini's tool-calling is more conservative than OpenAI's about what it will and won't call. A schema that works on the first try against gpt-4o will sometimes be rejected by Gemini with a "I can't use this tool" refusal because the parameter description is too vague. The fix is in the schema, not the code:
- Name tools with verb_noun.
get_weather,search_docs,create_invoice.weatherordoThingget skipped. - Describe every parameter.
city: z.string()works,city: z.string().describe("City name, e.g. 'Tokyo' or 'San Francisco'")works better. The description is what Gemini uses to decide when to call the tool. - Make tool descriptions specific. "Get the weather" is worse than "Get the current weather in Fahrenheit and wind speed in mph for a given city." A specific description gives the model a concrete reason to call the tool.
If a tool isn't getting called and you're sure the schema is valid, the description is the first place to look.
7. When to actually do this
For the 80% case — chat completions against a single model, with a handful of tools — this setup is good enough that you can ship it and not think about it. The OpenAI SDK is the most-used LLM client in the JavaScript ecosystem, and being able to swap providers with a one-line baseURL change is the actual reason Gemini ships an OpenAI-compatible endpoint in the first place.
For the remaining 20% — long-context retrieval, multimodal inputs, Gemini-specific features like Google's search grounding or code execution — you'll eventually want to use the official @google/genai SDK instead. It exposes things the OpenAI surface doesn't, and the function-calling shape is similar enough that you can write a thin adapter if you want the Agents SDK's orchestration on top.
For the multi-agent handoff case: it works. The Agents SDK's handoff is a chat-completions feature under the hood, and Gemini handles the multi-turn tool loops the same way OpenAI does. I've run a three-agent handoff against gemini-2.5-pro and the routing worked identically to gpt-4o on the same prompt.
8. The minimum viable setup, in full
If you skimmed everything above and just want a complete, working file to copy, this is the shortest one I'd ship. The plain simpleChat.js plus the agent.js from section 3 is the complete picture.
import {
Agent, Runner, tool, OpenAIProvider,
setDefaultOpenAIClient, setOpenAIAPI, setTracingDisabled,
} from "@openai/agents";
import OpenAI from "openai";
import "dotenv/config";
import { z } from "zod";
const client = new OpenAI({
apiKey: process.env.API_KEY,
baseURL: process.env.BASE_URL,
});
setDefaultOpenAIClient(client);
setOpenAIAPI("chat_completions");
setTracingDisabled(true);
const getWeather = tool({
name: "get_weather",
description: "Get the current weather for a city.",
parameters: z.object({
city: z.string().describe("City name"),
}),
async execute({ city }) {
return `Sunny in ${city}, 72°F.`;
},
});
const agent = new Agent({
name: "Assistant",
model: process.env.MODEL_NAME,
tools: [getWeather],
});
const runner = new Runner({
modelProvider: new OpenAIProvider({ openAIClient: client }),
});
const result = await runner.run(agent, "Weather in Tokyo?");
console.log(result.finalOutput);Put your three env vars in .env, run the file, and you have a working Gemini-backed agent in the OpenAI Agents SDK. If something here doesn't work for your setup, send me a note — I'd rather collect a few more failure modes in one place than have the next person waste a Wednesday on it.
Get new posts, in your inbox.
No list, no spam, no resale. Pick the categories you actually read, unsubscribe with a single click.
{/}
∑