Surfaces
Calling the gateway
OpenAI, Anthropic, Gemini, and curl: non-streaming, streaming, and cross-protocol.
Point the client you already use at your application gateway and pass an end-user ak_. The inbound wire (SDK + path) is independent of which entitled model the call reaches. That is what cross-protocol routing is.
Point the client
import OpenAI from "openai"
const client = new OpenAI({
baseURL: "https://<app>.gw.ourseams.com/v1",
apiKey: endUserKey,
})import Anthropic from "@anthropic-ai/sdk"
const client = new Anthropic({
baseURL: "https://<app>.gw.ourseams.com",
apiKey: endUserKey,
})import { GoogleGenAI } from "@google/genai"
const client = new GoogleGenAI({
apiKey: endUserKey,
httpOptions: { baseUrl: "https://<app>.gw.ourseams.com" },
})# OpenAI wire: base URL includes /v1
# Anthropic wire: host only (SDK appends /v1/messages)
# Gemini wire: host only; model lives in the pathAnthropic's base URL is the host without /v1. OpenAI's includes /v1. Gemini puts the model in the path (/v1beta/models/{model}:generateContent).
One call
const res = await client.chat.completions.create({
model: "acme/smart",
messages: [{ role: "user", content: "Say hello" }],
})
console.log(res.choices[0]?.message.content)const res = await client.messages.create({
model: "acme/smart",
max_tokens: 1024,
messages: [{ role: "user", content: "Say hello" }],
})
console.log(res.content)const res = await client.models.generateContent({
model: "acme/smart",
contents: "Say hello",
})
console.log(res.text)# openai.chat
curl https://<app>.gw.ourseams.com/v1/chat/completions \
-H "Authorization: Bearer ak_acme_…" \
-H "Content-Type: application/json" \
-d '{
"model": "acme/smart",
"messages": [{ "role": "user", "content": "Say hello" }]
}'
# anthropic.messages
curl https://<app>.gw.ourseams.com/v1/messages \
-H "Authorization: Bearer ak_acme_…" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "acme/smart",
"max_tokens": 1024,
"messages": [{ "role": "user", "content": "Say hello" }]
}'
# google.generate: slash in the model name is %2F
curl "https://<app>.gw.ourseams.com/v1beta/models/acme%2Fsmart:generateContent" \
-H "Authorization: Bearer ak_acme_…" \
-H "Content-Type: application/json" \
-d '{ "contents": [{ "parts": [{ "text": "Say hello" }] }] }'Streaming
Same clients, stream: true (or the SDK's stream helper). Spend is re-checked while chunks arrive; a ceiling can end the stream mid-answer.
const stream = await client.chat.completions.create({
model: "acme/smart",
messages: [{ role: "user", content: "Count to three." }],
stream: true,
})
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "")
}const stream = client.messages.stream({
model: "acme/smart",
max_tokens: 1024,
messages: [{ role: "user", content: "Count to three." }],
})
for await (const text of stream.textStream) {
process.stdout.write(text)
}const stream = await client.models.generateContentStream({
model: "acme/smart",
contents: "Count to three.",
})
for await (const chunk of stream) {
process.stdout.write(chunk.text ?? "")
}# openai.chat
curl https://<app>.gw.ourseams.com/v1/chat/completions \
-H "Authorization: Bearer ak_acme_…" \
-H "Content-Type: application/json" \
-N \
-d '{
"model": "acme/smart",
"stream": true,
"messages": [{ "role": "user", "content": "Count to three." }]
}'
# anthropic.messages
curl https://<app>.gw.ourseams.com/v1/messages \
-H "Authorization: Bearer ak_acme_…" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-N \
-d '{
"model": "acme/smart",
"max_tokens": 1024,
"stream": true,
"messages": [{ "role": "user", "content": "Count to three." }]
}'
# google.generate
curl "https://<app>.gw.ourseams.com/v1beta/models/acme%2Fsmart:streamGenerateContent" \
-H "Authorization: Bearer ak_acme_…" \
-H "Content-Type: application/json" \
-N \
-d '{ "contents": [{ "parts": [{ "text": "Count to three." }] }] }'Cross-protocol
Your customer can keep an OpenAI client while acme/claude resolves to Anthropic, or call Anthropic's Messages API against a model that lands on OpenAI. Only the gateway does that pivot; vendor APIs cannot speak another provider's wire.
| Inbound | Model provider | What happens |
|---|---|---|
| OpenAI chat / responses | Anthropic or Gemini model | Request and stream frames are translated |
| Anthropic messages | OpenAI or Gemini model | Same; caller still speaks Messages |
| Gemini generateContent | OpenAI or Anthropic model | Same; model stays in the path |
// OpenAI client, Anthropic (or Gemini) model behind the alias
const res = await client.chat.completions.create({
model: "acme/claude",
messages: [{ role: "user", content: "Say hello" }],
})
// res.model is the provider wire id; the caller still asked for acme/claude// Anthropic client, OpenAI model behind the alias
const res = await client.messages.create({
model: "acme/gpt",
max_tokens: 1024,
messages: [{ role: "user", content: "Say hello" }],
})// Gemini client, OpenAI model behind the alias
const res = await client.models.generateContent({
model: "acme/gpt",
contents: "Say hello",
})# OpenAI wire → whatever acme/claude resolves to at the provider
curl https://<app>.gw.ourseams.com/v1/chat/completions \
-H "Authorization: Bearer ak_acme_…" \
-H "Content-Type: application/json" \
-d '{
"model": "acme/claude",
"messages": [{ "role": "user", "content": "Say hello" }]
}'Where a field cannot survive the pivot, the gateway refuses or drops it rather than inventing fidelity. See lossy protocol conversions for the known list.
Tools and images
On the OpenAI wire, tools and images go through /v1/responses; chat.completions is text-only inbound. Anthropic and Gemini carry tools on their usual messages / generateContent shapes, including when the model that answers is a different provider.
// tools and images on the OpenAI wire use /v1/responses
const res = await client.responses.create({
model: "acme/claude",
input: "What is the weather in London?",
tools: [{
type: "function",
name: "get_weather",
description: "Get the weather for a city",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
}],
})const res = await client.messages.create({
model: "acme/gpt",
max_tokens: 1024,
tools: [{
name: "get_weather",
description: "Get the weather for a city",
input_schema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
}],
messages: [{ role: "user", content: "What is the weather in London?" }],
})const res = await client.models.generateContent({
model: "acme/gpt",
contents: "What is the weather in London?",
config: {
tools: [{
functionDeclarations: [{
name: "get_weather",
description: "Get the weather for a city",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
}],
}],
},
})curl https://<app>.gw.ourseams.com/v1/responses \
-H "Authorization: Bearer ak_acme_…" \
-H "Content-Type: application/json" \
-d '{
"model": "acme/claude",
"input": "What is the weather in London?",
"tools": [{
"type": "function",
"name": "get_weather",
"description": "Get the weather for a city",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}]
}'Protocols
| Id | Path |
|---|---|
openai.chat | POST /v1/chat/completions |
openai.responses | POST /v1/responses |
anthropic.messages | POST /v1/messages |
google.generate | POST /v1beta/models/{model}:generateContent (+ stream form) |