spicyapiDocs
Main content

Migrate from OpenAI or OpenRouter

Point existing OpenAI-library code at SpicyAPI. What changes (base URL, key, model ID), which request fields behave differently, and what to remove when you come from OpenRouter.

If your code already calls a chat model through the official OpenAI libraries, or through any tool with an "OpenAI-compatible" setting, moving it to SpicyAPI is three changes: the base URL, the API key and the model ID. This page lists those changes, then everything that behaves differently, so a migration does not end in a surprise 400. For complete example programs, see Chat recipes.

The three changes

SettingValue on SpicyAPI
Base URLhttps://api.spicyapi.ai/v1
API keyA SpicyAPI key (starts with sk-spicy-), sent as Authorization: Bearer
ModelAn exact text model ID from the live catalog, for example in SPICY_MODEL (see Model IDs)
import os
from openai import OpenAI

# Before: client = OpenAI()   (reads OPENAI_API_KEY, calls api.openai.com)
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")

reply = client.chat.completions.create(
    model=os.environ["SPICY_MODEL"],
    messages=[{"role": "user", "content": "Say hello in one sentence."}],
)
print(reply.choices[0].message.content)

The OpenAI libraries also read OPENAI_BASE_URL and OPENAI_API_KEY from the environment. Setting those two lets existing code run unchanged, but it also redirects every other OpenAI call in the same process, so prefer passing the values explicitly as above.

Model IDs

Text model IDs have the form publisher/model/chat. Copy them from the live catalog rather than translating a name you know from elsewhere:

  • GET /api/v1/models?modality=text lists only text models, with their prices, and adds each model's inputSchema with includeSchema=1. See Model catalog and schemas.
  • GET /v1/models is the OpenAI-format list that tools read. Without a filter it includes image, video and audio models, which cannot be called on /v1/chat/completions; add ?modality=text for text models only. Besides the four OpenAI fields, each item has modality and task, and chat models also have context_length, max_output_tokens and supported_parameters (the request fields that model accepts). The OpenAI libraries ignore the extra fields.

A model ID that does not exist returns 404 with code: "model_not_found" and param: "model"; the message shows the ID shape and, when there is one, the closest callable chat model. Calling an image or video model on a chat endpoint returns 400 saying it is not a chat model. IDs from other services often look similar but are not interchangeable: publisher names are spelled differently, suffixes such as :free do not exist here, and text models end in /chat.

Request fields

Each model publishes the fields it accepts in its inputSchema (and, by name, in supported_parameters on /v1/models).

  • Sampling fields the model does not list are ignored. temperature, top_p, top_k, min_p, top_a, frequency_penalty, presence_penalty and repetition_penalty only change how random or repetitive a reply is, and many clients send them by default, so a model that does not take them simply generates without them. The same goes for n: 1, an empty stop, stop on a model without stop sequences, and reasoning_effort on a model without reasoning levels. The response tells you what was ignored in the X-Spicy-Ignored-Params header, for example frequency_penalty, presence_penalty.
  • Values outside what the model takes are changed to the nearest accepted value. A sampling value outside the model's range is clamped to the bound, a stop list longer than the model allows keeps its first entries, and a reasoning_effort level the model does not offer becomes the nearest one it does (the lower one on a tie). The response lists each change as name=value in the X-Spicy-Adjusted-Params header, for example reasoning_effort=low, temperature=1.
  • Any other field the model does not list is rejected with 400, code: "unsupported_parameter", param set to the field, and a message that names it, for example logprobs: is not a supported field. These fields change what you get back, so you never pay for a reply generated without one you thought you had sent.
FieldOn SpicyAPI
messagesSame shape. Roles system, developer, user, assistant and tool. Text content works everywhere; image_url parts only on models whose schema lists them
max_tokens, max_completion_tokensBoth accepted, same meaning. Reasoning tokens count toward it. Each model has its own default and ceiling
stream, stream_optionsSupported, with OpenAI's usage rules: a stream carries usage only when include_usage is true (see Responses and streams)
tools, tool_choiceSupported on every text model. Some models accept only auto and none for tool_choice, so forcing a named tool returns 400 there
parallel_tool_callsOnly on models that list it
reasoning_effortOn most models, but the allowed values differ by model: some add none, minimal, xhigh or max, and some have no medium. A value the model does not offer is changed to the nearest one it does, so read the model's enum if the exact level matters
temperature, top_p, top_k, frequency_penalty, presence_penaltyUsed on models that list them, clamped to the model's range, ignored on the others (named in X-Spicy-Ignored-Params). Several models take none of the sampling fields
seedOnly on models that list it
stopUsed on models that list it, trimmed to the model's maximum count; ignored on models without stop sequences and when empty
response_formatjson_object on models that list the field; not available on the others
user, metadata, storeAccepted and ignored
n1 is ignored; any other value is rejected with 400 on models that do not list it
logprobs, top_logprobs, logit_bias, and any other field the model does not listRejected with 400

To see a model's fields and their allowed values before you send anything, run the command in Chat recipes: before you start.

Responses and streams

A non-streamed reply is a normal chat.completion object, so reply.choices[0].message.content and reply.usage work as before. The differences are small:

  • reasoning_content. Models that think before answering return that text in message.reasoning_content (or delta.reasoning_content when streaming), next to content. It is already counted in usage.completion_tokens.
  • The ID is the request ID. id has the form chatcmpl-req_..., and the x-request-id response header carries the same req_... value. Quote it when you contact support. A second header, X-Spicy-Task-Id, names the task that holds the charge (see Cost and usage).
  • No system_fingerprint and no logprobs.

Streams use the same chat.completion.chunk events and end with data: [DONE], and follow OpenAI's rules for usage:

  • Usage only on request. Without stream_options.include_usage, no chunk carries usage and every chunk has a non-empty choices list. With "include_usage": true, one extra chunk with an empty choices list and the final usage arrives just before [DONE], and every other chunk has usage: null. Code written against OpenAI's streaming examples works unchanged; checking choices before reading choices[0] keeps it safe on that last chunk.
  • One finish_reason per choice. Each choice ends with exactly one chunk that carries its finish_reason, whichever model you call.
  • Keep-alive lines. While a model is still thinking, the stream carries : keep-alive comment lines so the connection stays open. The OpenAI libraries skip them; a hand-written parser must too.

For long replies or high reasoning_effort, stream. A non-streamed request that runs past about 30 seconds is kept open with whitespace ahead of the JSON body and the X-Spicy-Keepalive: 1 header, so the OpenAI libraries still parse it; but if it fails after that point, the error object arrives in the body of a 200 response, which the libraries do not raise as an error. See Timeouts and streaming.

Errors

Errors use the OpenAI shape, {"error": {"message", "type", "param", "code"}}, with the usual HTTP status codes, so the OpenAI libraries raise their usual exceptions. param names the field the error is about, as a dot path such as temperature or messages.1.content.0, and code separates errors that share a status: unsupported_parameter (a field or message part the model does not accept), invalid_request (a missing field or a value the model does not accept), content_rejected (a content-policy refusal, also 400) and model_not_found (404). One difference from OpenAI: 402 is used when your balance or a spend cap does not cover the request. The full table, with what to do about each code, is in Chat recipes: when something goes wrong.

The OpenAI libraries retry 429 and 5xx responses on their own. That is safe here: a request that fails is not charged. If you send an Idempotency-Key, pass it per call and read Idempotency keys on text endpoints first.

Cost and usage

Replies report token counts in usage, not a price. Prices are per million tokens for input, cached input and output, listed on each model's page and in the catalog. To find what a call actually cost, read the X-Spicy-Task-Id response header (job_…), present on every accepted request, streamed or not, and look the task up with GET /api/v1/jobs/recordInfo?taskId=<job_…>: cost is final once settled is true. Items in GET /api/v1/jobs also carry the requestId of the call that created them (the X-Request-Id header), and Console → Logs shows the same tasks. Reading the header from the OpenAI libraries is shown in Chat recipes: find what a call cost; see also Quotes and billing.

Before a request runs, an amount is held from your balance that covers your messages plus the full max_tokens of output (the model's default when you leave it out). You pay only for what is used, but the hold has to fit first, so a small balance combined with a large max_tokens returns 402. Set a realistic max_tokens, or set spend caps on the key; see Billing.

Endpoints you will not find here

Text generation is available as OpenAI Chat Completions, OpenAI Responses, Anthropic Messages and Google Gemini; the details are in Text and streaming. These OpenAI endpoints do not exist and return 404: /v1/embeddings, the legacy /v1/completions, /v1/images/*, /v1/audio/*, /v1/moderations, /v1/files, /v1/batches, /v1/assistants and fine-tuning. Image, video and audio models are called through the task API instead, and videos also through /v1/videos.

Coming from OpenRouter

OpenRouter also speaks the OpenAI format, so the three changes above apply: replace the base URL https://openrouter.ai/api/v1 with https://api.spicyapi.ai/v1, and swap the key and the model IDs. Then remove the fields that exist only on OpenRouter; here they return 400:

OpenRouter fieldWhat to do
models (a fallback list) and routeRemove. Send one model ID; SpicyAPI chooses how to serve it
providerRemove. There is no per-request provider selection
reasoning (an object such as {"effort": "high"})Translated to the top-level reasoning_effort (enabled: false becomes none where the model has it); other keys inside it are ignored. Sending reasoning_effort directly is clearer
usage ({"include": true})Remove. Non-streamed replies always include usage; for streams send stream_options: {"include_usage": true}
transforms, pluginsRemove

The HTTP-Referer and X-Title headers are harmless and ignored.

Checklist

  1. Base URL https://api.spicyapi.ai/v1 and a SpicyAPI key.
  2. Model ID copied from the live catalog, ending in /chat.
  3. Request fields checked against the model's inputSchema; unsupported non-sampling fields removed, and the response headers X-Spicy-Ignored-Params and X-Spicy-Adjusted-Params checked once to see what was changed.
  4. If you send stream_options.include_usage, stream loops skip the final chunk whose choices list is empty.
  5. max_tokens set to a realistic value, or left to the model's default, with spend caps on the key.
  6. Long or high-effort requests use stream: true.

Continue reading

Chat recipes · Text and streaming · Model catalog and schemas · Authentication · Billing

On this page