Skip to main content

Integration guide

Keep your client. Change the base URL and the model value.

Text models here speak the protocols your SDK already speaks: Chat Completions, the stateless Responses subset, Anthropic Messages and Google generateContent. The interesting question is what happens to a parameter the target model never declared.

The challenge

Compatibility layers usually lie by omission

The classic failure is not a 500. It is a parameter your gateway accepted, dropped and never mentioned, so the model ran without your temperature, your tool definitions or your response format — and the output looked merely disappointing instead of broken.

01

Undeclared parameters are rejected, not dropped

messages, tools, tool_choice, response_format, max_tokens, temperature and top_p map onto the model's declared fields. Anything else is forwarded to the model's schema and returns 400 when the model does not declare it. Silence is never the answer.

02

The error shape follows the protocol you called

On /v1 the body is an OpenAI or Anthropic error object, not the {code, msg, data, request_id} envelope used by /api/v1. Handle both if your product mixes the two surfaces.

03

Streaming and settlement are separate concerns

stream: true returns chat.completion.chunk events terminated by data: [DONE]. Billing still follows measured usage, so an aborted stream is not a free request.

The approach

Swap the endpoint, then re-run your evals

Change base_url and api_key, replace the model string with a value from GET /v1/models, and run the test suite you already have. Where a 400 comes back for a parameter, that parameter was never supported by the model you picked — which is information the silent version never gave you.

Any OpenAI-compatible client works. Only the base URL and the model value change.

POST /v1/chat/completions

from openai import OpenAI

client = OpenAI(
    base_url="https://api.spicyapi.ai/v1",
    api_key=os.environ["SPICY_API_KEY"],
)

stream = client.chat.completions.create(
    model="<publisher>/<model>/chat",          # exact value from GET /v1/models
    messages=[{"role": "user", "content": "Summarise these notes in three bullets."}],
    temperature=0.7,
    stream=True,
)
for chunk in stream:
    print(chunk.choices[0].delta.content or "", end="")

text/event-stream

data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"- "}}]}
data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"Ships"}}]}
data: [DONE]

Contract

The parts worth pinning down

OpenAI-compatible LLM API: swap the base URL — The parts worth pinning down
Field or headerValue or limit
POST /v1/chat/completionschat.completion · SSE with stream: true
GET /v1/modelsevery callable model value
Authorization: Bearer · x-api-key · x-goog-api-keyaccepted on /v1
undeclared parameter400 · never silently dropped

Swipe horizontally to see all columns.

Integration

From first call to production

  1. 01Create a key, then GET /v1/models for the identifiers that are callable right now.
  2. 02Point the client at the base URL and send one non-streaming request; the response is a standard chat.completion object.
  3. 03Turn streaming on and confirm your reader handles chat.completion.chunk and the [DONE] sentinel.
  4. 04Watch usage per key in the console and set a daily cap before routing production traffic.

Fit

When this path is the right one

Choose this when

  • You already have working OpenAI, Anthropic or Google client code and want a different model behind it.
  • You want image, video and text on one key, one balance and one set of spend caps.

Look elsewhere when

  • You depend on stateful server-side threads or assistants. The Responses support here is the stateless subset.
  • You need media output. That goes through createTask, or the Videos projection for clients that already speak it.

Evidence

What you can verify yourself

Four protocol surfaces, one key

/v1/chat/completions, /v1/responses, /v1/messages and /v1beta/models/{model}:generateContent all authenticate with the same key. Authorization: Bearer, x-api-key and x-goog-api-key are each accepted where the SDK expects them.

The model list is the contract

GET /v1/models returns exactly the identifiers every /v1 and /api/v1 operation accepts, with the publisher in owned_by. Prices come from /api/v1/models, which is account-specific and therefore private.

Validation, limits and holds are shared

The compatibility layer reuses the same authentication, rate limits, validation, pricing and hold logic as the native surface. Only the envelope differs.

Before launch

Platform behaviour to confirm

  • Keep the key server-side; a credential in the URL query string is never accepted.
  • Log the protocol's own response id with every call so support requests have a handle.
  • Decide how a 400 on an unsupported parameter reaches your users before you ship.
  • Set per-key daily, monthly and lifetime caps instead of relying on the balance alone.

FAQ

Questions this page gets asked

Any client that lets you set a base URL and a model string. OpenAI-shaped clients use /v1/chat/completions and /v1/responses, Anthropic clients use /v1/messages, and Google GenAI clients use the generateContent paths.

Keep going

Point a client at a text model

Point an existing OpenAI, Anthropic or Google client at SpicyAPI. What maps straight through, what is rejected on purpose, and how text is billed.

Related guides