spicyapiDocs
Main content

Text and streaming

Call text models in the official OpenAI, Anthropic or Google Gemini format, and handle conversations, tools, streams and costs correctly.

Select an available text model

Read the authenticated /api/v1/models?modality=text&includeSchema=1 catalog and require enabled and available. Tools, multimodal messages, reasoning fields and structured output must also be supported by that model’s inputSchema. Protocol compatibility does not give every model every capability.

The same model ID works with any of the protocols below, regardless of who built the model: you can call another vendor's model in Anthropic format, or a non-Google model in Gemini format.

Supported protocols

All four text protocols share the same API key and model catalog, along with the same rate limits, parameter validation and billing; only the request and response formats differ. Use whichever one your existing code or SDK already speaks.

ProtocolEndpointAPI key header
OpenAI Chat CompletionsPOST /v1/chat/completionsAuthorization: Bearer
OpenAI ResponsesPOST /v1/responsesAuthorization: Bearer
Anthropic MessagesPOST /v1/messagesx-api-key or Authorization: Bearer
Google GeminiPOST /v1beta/models/{model}:generateContentx-goog-api-key or Authorization: Bearer
Google Gemini (streaming)POST /v1beta/models/{model}:streamGenerateContentx-goog-api-key or Authorization: Bearer
Native streamPOST /api/v1/jobs/streamAuthorization: Bearer

On /v1 and /v1beta, responses and errors follow their own protocols and carry no native {code,msg,data,request_id} envelope, so do not parse them with a client that only reads body.data. The Gemini format puts the model ID in the URL path; slashes in the ID stay as they are and need no encoding.

Send the key in a request header only. x-api-key and x-goog-api-key work only on /v1 and /v1beta; the native /api/v1 accepts only Authorization: Bearer. A ?key= query parameter is never accepted: once a key lands in a URL, it lingers in browser history, proxy servers and access logs. The official SDKs all send the key in a header, so they are unaffected.

Use an official SDK

Change only the base URL and the key; everything else works the way each SDK normally does.

SDKBase URL
OpenAI (openai)https://api.spicyapi.ai/v1
Anthropic (anthropic)https://api.spicyapi.ai
Google GenAI (google-genai)https://api.spicyapi.ai

The Anthropic and Google SDKs append /v1 or /v1beta themselves, so their base URL has no version path; the OpenAI SDK's base URL must include /v1. All three Python examples below read SPICY_API_KEY from the environment, along with a SPICY_MODEL chosen from the live catalog.

OpenAI
import os
from openai import OpenAI

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": "Explain a rainbow in one sentence."}],
)
print(reply.choices[0].message.content)
Anthropic
import os
import anthropic

client = anthropic.Anthropic(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai")
message = client.messages.create(
    model=os.environ["SPICY_MODEL"],
    max_tokens=256,
    messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
)
print(message.content[0].text)
Google GenAI
import os
from google import genai
from google.genai import types

client = genai.Client(
    api_key=os.environ["SPICY_API_KEY"],
    http_options=types.HttpOptions(base_url="https://api.spicyapi.ai"),
)
response = client.models.generate_content(
    model=os.environ["SPICY_MODEL"],
    contents="Explain a rainbow in one sentence.",
)
print(response.text)

To keep network retries from charging twice, generate one Idempotency-Key per user action, persist it, and send it through the SDK's custom-header option (extra_headers in the OpenAI and Anthropic SDKs).

Parameter mapping across protocols

Protocol-level fields are first converted to the platform's canonical parameter names, then validated against the selected model's inputSchema. A parameter the model does not support returns 400; it is never silently dropped while the request is billed as usual. The table below shows the mapping only, and "—" means the protocol has no equivalent field. Whether a given model accepts a parameter, and which values it allows, is defined by that model's inputSchema.

Canonical parameterChat CompletionsResponsesMessagesGemini
messagesmessagesinstructions, inputsystem, messagessystemInstruction, contents
max_tokensmax_tokens, max_completion_tokensmax_output_tokensmax_tokensgenerationConfig.maxOutputTokens
temperaturetemperaturetemperaturetemperaturegenerationConfig.temperature
top_ptop_ptop_ptop_pgenerationConfig.topP
toolstoolstools (function type)tools (custom tools)tools[].functionDeclarations
tool_choicetool_choicetool_choicetool_choicetoolConfig.functionCallingConfig
response_formatresponse_formattext.formatgenerationConfig.responseMimeType plus responseSchema or responseJsonSchema
reasoning_effortreasoning_effortreasoning.effortgenerationConfig.thinkingConfig

Fields not listed in the table are likewise converted to canonical parameters and validated by the model; anything missing from its inputSchema returns 400. For example, if a model does not accept stop, then Chat's stop, Messages' stop_sequences and Gemini's generationConfig.stopSequences all return 400. No text model currently lists seed in its inputSchema, so Chat's seed and Gemini's generationConfig.seed return 400 as well.

A few exceptions:

  • Fields that only mean something to the protocol's own platform are ignored and do not affect generation: user, metadata and store in Chat; user, metadata, store and include in Responses; metadata in Messages.
  • Responses does not support previous_response_id; send the full history in input.
  • Gemini's safetySettings is accepted but has no effect. cachedContent and server-side tools such as googleSearch and codeExecution are not supported and return 400. Pass images as inlineData (base64) or fileData.fileUri (an https URL); whether a model accepts images is defined by its inputSchema.

Send a Chat stream

On your server, set SPICY_API_KEY, a SPICY_MODEL obtained from the live catalog, and a persisted SPICY_IDEMPOTENCY_KEY for this business action. The example needs jq. Replace the sample prompt as needed, and validate model parameters against the current schema before sending.

jq -n --arg model "$SPICY_MODEL" '{
  model: $model,
  messages: [{role: "user", content: "Explain a rainbow in one sentence."}],
  stream: true
}' > chat-request.json

curl --fail-with-body --no-buffer --max-time 120 \
  https://api.spicyapi.ai/v1/chat/completions \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
  --data-binary @chat-request.json

Send a Gemini request

Use the same environment variables as in the previous section. The model ID goes in the URL, so the request body carries no model field. Wrap the URL in double quotes and write the variable as ${SPICY_MODEL}, so the shell does not read the colon that follows it as a variable modifier.

jq -n '{
  systemInstruction: {parts: [{text: "Answer in one sentence."}]},
  contents: [{role: "user", parts: [{text: "Explain a rainbow."}]}],
  generationConfig: {temperature: 0.7, maxOutputTokens: 256}
}' > gemini-request.json

curl --fail-with-body --no-buffer --max-time 120 \
  "https://api.spicyapi.ai/v1beta/models/${SPICY_MODEL}:streamGenerateContent?alt=sse" \
  -H "x-goog-api-key: $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
  --data-binary @gemini-request.json

If you do not need streaming, replace :streamGenerateContent?alt=sse with :generateContent and read the answer from candidates[0].content.parts[].text; usage is in usageMetadata. Without alt=sse, a streaming request returns a single JSON array written out incrementally; the official SDKs add alt=sse for you.

Conversations and tool calls

For non-streaming Chat, read choices[0].message; for a stream, process choices[].delta by SSE event, not by TCP chunk. Assemble tool_calls by index before validating and executing complete arguments. Preserve the assistant message and return results with the matching tool_call_id. Responses requires the full history in input and rejects previous_response_id; return function results as function_call_output with the matching call_id. Messages uses text, tool_use and tool_result content blocks rather than the Chat message shape.

Gemini contents alternate between two roles, user and model. When the model wants to call a tool, it returns a functionCall part; after running the tool, wrap the result in a functionResponse part and send it back in the next user content, using the same name as the call.

Completion, errors and disconnects

ProtocolNormal completionUsage
Chat Completionsdata: [DONE]usage in the last chunk
Responsesresponse.completed eventresponse.usage in that event
Messagesmessage_stop after message_deltausage in message_delta
GeminiLast event with finishReason; there is no [DONE]usageMetadata

Check the HTTP status and Content-Type before reading a stream; errors come back as each protocol's own JSON:

ProtocolError body
Chat Completions, Responses{"error": {"message", "type", "param", "code"}}
Messages{"type": "error", "error": {"type", "message"}}
Gemini{"error": {"code", "message", "status"}}

HTTP status codes are the same across all four protocols. Gemini's status uses Google's status names: 400 is INVALID_ARGUMENT, 401 is UNAUTHENTICATED, 402 is FAILED_PRECONDITION, 403 is PERMISSION_DENIED, 404 is NOT_FOUND, 429 is RESOURCE_EXHAUSTED and 503 is UNAVAILABLE. Branch your code on the status code and error type, never on the message text.

If something fails after streaming begins, Chat and Gemini send an event carrying an error object, Responses sends an error event and Messages sends event: error. Your client must handle these error events, as well as timeouts and premature EOF. Disable response buffering in your proxy. Closing a browser or firing an AbortSignal only stops reception; it does not cancel the task or promise a refund. A client disconnect with trustworthy usage is settled against work already performed; absent usage does not make a closed connection a free completion.

Quotes and billing

For explicit cost confirmation, use native jobs/quote → jobs/stream with the same model/input plus quoteId and expectedCost. /v1 and /v1beta do not accept those native quote credentials; they apply the price at acceptance. Use task records and billing history for final charges, not an end-of-text marker. reasoning_tokens is already included in completion_tokens: do not add it twice.

Other request shapes

These examples describe protocol envelopes only. Replace MODEL_ID_FROM_CATALOG with a live model that supports the fields shown; never send the placeholder itself.

POST /v1/responses
{
  "model": "MODEL_ID_FROM_CATALOG",
  "input": "Explain a rainbow in one sentence.",
  "stream": false
}
POST /v1/messages
{
  "model": "MODEL_ID_FROM_CATALOG",
  "max_tokens": 256,
  "messages": [{"role": "user", "content": "Explain a rainbow in one sentence."}],
  "stream": false
}

Continue reading

Quotes and protocol compatibility · Billing · Production integration · Troubleshooting and recovery

On this page