Most LLM projects end up talking to more than one vendor: GPT for one job, Claude for another, Gemini or DeepSeek when cost matters. Each vendor has its own SDK, key, billing account and rate limits. An OpenAI-compatible gateway removes most of that: one base URL, one key, and the model is just a string.
This post shows the pattern with Gisoo, the gateway we run (disclosure: I'm on the team). It exposes 680+ models behind the standard OpenAI API shape, so the official openai SDKs work unchanged. The same code works with any OpenAI-compatible gateway; only the base URL and key change.
curl -s https://gisoo.pro/api/v1/models | jq '.data | length'
curl -s https://gisoo.pro/api/v1/models \
| jq -r '.data[] | select(.type=="chat") | .id' | head
Each entry has an id, an owned_by vendor and a type (chat, image, audio, embedding, video, music). Directly connected models use their own name ( gpt-5-mini, deepseek-flash, gemini-3.6-flash); the rest carry an openrouter/ prefix (openrouter/anthropic/claude-sonnet-4.5).
from openai import OpenAI
client = OpenAI(
base_url="https://gisoo.pro/api/v1",
api_key="sk-gisoo-v1-...", # created in the Gisoo app
)
question = "Explain idempotency keys in two sentences."
for model in ["gpt-5-mini", "gemini-3.6-flash", "deepseek-flash"]:
r = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": question}],
)
print(f"--- {model}\n{r.choices[0].message.content}\n")
Switching vendor is a one-word change. That makes it cheap to A/B models on your own prompts instead of trusting benchmarks.
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://gisoo.pro/api/v1",
apiKey: process.env.GISOO_API_KEY,
});
const stream = await client.chat.completions.create({
model: "openrouter/anthropic/claude-sonnet-4.5",
messages: [{ role: "user", content: "Write a haiku about retries." }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
Streaming is plain SSE in the OpenAI format, so existing UI code (Vercel AI SDK, LangChain, LiteLLM clients) keeps working.
Because every model sits behind the same endpoint, fallback is a loop, not three SDKs:
from openai import OpenAI, APIError
client = OpenAI(base_url="https://gisoo.pro/api/v1", api_key="sk-gisoo-v1-...")
def ask(messages, models=("deepseek-flash", "gemini-3.6-flash", "gpt-5-mini")):
last = None
for m in models:
try:
return m, client.chat.completions.create(model=m, messages=messages, timeout=60)
except APIError as e:
last = e # vendor busy or refused: try the next one
raise last
model, r = ask([{"role": "user", "content": "Summarise RFC 9110 in one line."}])
print(model, r.choices[0].message.content)
Order the list cheapest-first and you only pay for the expensive model when the cheap one fails.
The same key covers the other OpenAI endpoints:
| Task | Endpoint |
|---|---|
| Chat / tools / JSON output | POST /chat/completions ,POST /responses |
| Embeddings | POST /embeddings |
| Images | POST /images/generations |
| Speech-to-text / text-to-speech | POST /audio/transcriptions ,POST /audio/speech |
| Video | POST /videos |
There is also an Anthropic-compatible endpoint (https://gisoo.pro/api) for tools that only speak the Anthropic Messages API, and a remote MCP server (https://gisoo.pro/api/mcp) for Claude Code, Cursor and Cline.
Gisoo started for developers and teams in Iran, who can't pay OpenAI, Anthropic or Google directly: credit is paid in local currency, and there is an Iran-optimised mirror at ir.gisoo.pro. The API itself is the standard one, so the code above runs from anywhere.
Docs (Persian, code is universal): https://gisoo.pro/docs. Questions and corrections are welcome in the comments.