spicyapiDokumentacja
Tekst i czat
Treść główna

Tekst i streaming

Wywołuj modele tekstowe w oficjalnym formacie OpenAI, Anthropic lub Google Gemini i poprawnie obsługuj rozmowy, narzędzia, strumienie oraz koszty.

Modele tekstowe odpowiadają w ramach tego samego żądania HTTP: nie ma zadania, które trzeba odpytywać. Możesz poczekać na całą odpowiedź albo ją streamować, czyli odbierać tekst małymi kawałkami, gdy model jeszcze pisze – tak jak aplikacja czatu wypisuje odpowiedź na bieżąco. Kawałki strumienia przychodzą jako Server-Sent Events (SSE): zwykłe linijki tekstu zaczynające się od data:, wysyłane jedna po drugiej przez połączenie, które pozostaje otwarte, dopóki odpowiedź nie będzie kompletna.

Wybierz dostępny model tekstowy

Przykłady dla powłoki na tej stronie są napisane dla terminala w macOS lub Linuksie i korzystają z jq – małego narzędzia wiersza poleceń do pracy z JSON (brew install jq w macOS, sudo apt install jq w Debianie lub Ubuntu). W Windows uruchamiaj je w WSL albo skorzystaj z przykładów dla SDK w Pythonie.

Najpierw umieść klucz API w zmiennej środowiskowej, czyli nazwanej wartości, którą terminal przekazuje uruchamianym poleceniom – dzięki temu klucza nigdy nie trzeba wklejać do polecenia:

export SPICY_API_KEY="sk-spicy-paste-your-key-here"

Wyświetl listę modeli tekstowych, które ten klucz może wywołać w tej chwili:

curl -s "https://api.spicyapi.ai/api/v1/models?modality=text" \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  | jq -r '.data.items[] | select(.enabled and .available) | .model'

Powinien pojawić się jeden identyfikator modelu w każdej linijce. Skopiuj jeden z nich i zapisz go w SPICY_MODEL; odczytuje go każdy przykład poniżej:

export SPICY_MODEL="MODEL_ID_FROM_CATALOG"   # wstaw tu jedną linijkę z listy powyżej

MODEL_ID_FROM_CATALOG to tylko symbol zastępczy – nigdy nie wysyłaj go dosłownie. Zanim zaczniesz polegać na narzędziach, obrazach, ustawieniach rozumowania albo ustrukturyzowanym wyjściu, przeczytaj inputSchema tego modelu z GET /api/v1/models/{model}: te funkcje działają tylko wtedy, gdy schemat je wymienia. Zgodność protokołu nie daje każdemu modelowi wszystkich możliwości.

Ten sam identyfikator modelu działa z każdym z poniższych protokołów, niezależnie od tego, kto zbudował model: możesz wywołać model innego producenta w formacie Anthropic albo model spoza Google w formacie Gemini.

Obsługiwane protokoły

Wszystkie te endpointy korzystają z tego samego klucza API i katalogu modeli, a także z tych samych limitów żądań, walidacji parametrów i rozliczeń; różnią się tylko formatem żądań i odpowiedzi. Wybierz ten, którym już posługuje się twój kod albo SDK.

ProtokółEndpointNagłówek z kluczem API
OpenAI Chat CompletionsPOST /v1/chat/completionsAuthorization: Bearer
OpenAI ResponsesPOST /v1/responsesAuthorization: Bearer
Anthropic MessagesPOST /v1/messagesx-api-key lub Authorization: Bearer
Google GeminiPOST /v1beta/models/{model}:generateContentx-goog-api-key lub Authorization: Bearer
Google Gemini (streaming)POST /v1beta/models/{model}:streamGenerateContentx-goog-api-key lub Authorization: Bearer
Natywny strumień z potwierdzoną cenąPOST /api/v1/jobs/streamAuthorization: Bearer

Na /v1 i /v1beta odpowiedzi i błędy są zgodne z własnymi protokołami i nie mają natywnej struktury {code,msg,data,request_id}, więc nie parsuj ich klientem, który czyta tylko body.data. Format Gemini umieszcza identyfikator modelu w ścieżce adresu URL; ukośniki w identyfikatorze zostają bez zmian i nie trzeba ich kodować.

Klucz wysyłaj wyłącznie w nagłówku żądania. x-api-key i x-goog-api-key działają tylko na /v1 i /v1beta; natywne /api/v1 przyjmuje wyłącznie Authorization: Bearer. Parametr zapytania ?key= nigdy nie jest przyjmowany: klucz, który raz trafi do adresu URL, zostaje w historii przeglądarki, na serwerach proxy i w logach dostępu. Wszystkie oficjalne SDK wysyłają klucz w nagłówku, więc ich to nie dotyczy.

Użyj oficjalnego SDK

Zmień tylko adres bazowy i klucz; cała reszta działa tak, jak zwykle w danym SDK.

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

SDK Anthropic i Google same dopisują /v1 albo /v1beta, więc ich adres bazowy nie zawiera ścieżki wersji; adres bazowy SDK OpenAI musi zawierać /v1. Wszystkie trzy przykłady w Pythonie poniżej odczytują ze środowiska SPICY_API_KEY oraz SPICY_MODEL wybrany z katalogu na żywo.

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)

Warstwa zgodności nalicza cenę w chwili przyjęcia i nie przyjmuje wyceny

/v1 i /v1beta pobierają cenę obowiązującą w chwili przyjęcia żądania. Nie przyjmują quoteId ani expectedCost, więc na tej ścieżce pojedyncze wywołanie nie ma górnej granicy – pętla agenta, która wymknie się spod kontroli, wydaje dalej.

Są dwa sposoby, żeby to ograniczyć, a większość integracji potrzebuje obu:

  • Każde wywołanie z osobna – pobierz wycenę natywnym POST /api/v1/jobs/quote, a potem wyślij POST /api/v1/jobs/stream ze zwróconymi quoteId i expectedCost. Jeśli cena w międzyczasie się zmieniła, przyjęcie nie udaje się z kodem 40901, zamiast pobrać więcej, niż zostało zaakceptowane. Zobacz Natywny strumień z potwierdzoną ceną.
  • Wszystko, co wydaje ten klucz – ustaw dzienny, miesięczny i łączny limit wydatków klucza. Obowiązują na każdej ścieżce, także na tej, a przekroczenie zwraca 40202 zamiast wyższego rachunku. Zobacz Uwierzytelnianie.

Klucze idempotentności na endpointach tekstowych

Nagłówek Idempotency-Key sprawia, że ponowienie po błędzie sieci nie pobierze opłaty dwa razy. Na endpointach tekstowych nie pozwala jednak odzyskać utraconej odpowiedzi, bo odpowiedź jest do ciebie streamowana i nie jest zapisywana:

  • Ponowne wysłanie klucza, którego żądanie się zakończyło, z tą samą treścią zwraca 400 (This Idempotency-Key was already used for a completed request, and its answer cannot be replayed. Use a new Idempotency-Key to send a new request) i nic więcej nie pobiera. Zapis zadania również nie będzie zawierał tekstu.
  • Jeśli tamto wcześniejsze żądanie się nie powiodło, ponowne wysłanie tego samego klucza z tą samą treścią zwraca ten sam błąd co wtedy i niczego nie pobiera.
  • Ponowne wysłanie klucza z inną treścią zwraca 409.

Dlatego wysyłaj nowy klucz z każdą nową wiadomością (na przykład $(uuidgen) w powłoce, uuid.uuid4() w Pythonie), a tego samego klucza używaj ponownie tylko wtedy, gdy po błędzie sieci wysyłasz dokładnie to samo żądanie – godząc się z tym, że odpowiedzi na to żądanie nie da się odzyskać. Jeśli chcesz dostać odpowiedź jeszcze raz, wyślij żądanie z nowym kluczem i zapłać za nie.

W SDK przekazuj klucz przy każdym wywołaniu, nigdy w obiekcie klienta – przy jednym kluczu dla całego klienta każda wiadomość po pierwszej byłaby przez 24 godziny odrzucana z kodem 409:

Idempotency-Key dla każdego wywołania
import os, uuid

# SDK OpenAI (client z powyższego przykładu OpenAI). SDK Anthropic przyjmuje
# ten sam argument extra_headers w client.messages.create().
reply = client.chat.completions.create(
    model=os.environ["SPICY_MODEL"],
    messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
    extra_headers={"Idempotency-Key": str(uuid.uuid4())},
)

# SDK Google GenAI (client i types z powyższego przykładu Google GenAI):
# http_options w konfiguracji pojedynczego wywołania
response = client.models.generate_content(
    model=os.environ["SPICY_MODEL"],
    contents="Explain a rainbow in one sentence.",
    config=types.GenerateContentConfig(
        http_options=types.HttpOptions(headers={"Idempotency-Key": str(uuid.uuid4())}),
    ),
)

SDK OpenAI i Anthropic same ponawiają żądania po przekroczeniu czasu, z tymi samymi nagłówkami. Jeśli takie ponowienie trafi na żądanie, które już przyjęliśmy, SDK zgłasza błąd 400 (bad request) z powyższym komunikatem: pierwsza próba została przetworzona i rozliczona, a jej odpowiedź przepadła.

Modele SpicyAPI w kliencie wiersza poleceń Grok

Ta konfiguracja sprawia, że klient wiersza poleceń firmy xAI wysyła swoje żądania do SpicyAPI: od tej pory rozmawia z modelami z katalogu SpicyAPI, a nie z własnym endpointem xAI. Należy do nich także rodzina modeli Grok opublikowana w katalogu — wywołujesz ją przez jej identyfikator modelu w SpicyAPI, tak samo jak każdy inny model. Klient może skierować własny model na dowolny endpoint zgodny z OpenAI. Dodaj poniższy wpis do ~/.grok/config.toml; env_key podaje nazwę zmiennej środowiskowej, którą czyta klient, więc sam klucz nigdy nie trafia do pliku.

~/.grok/config.toml
[model.spicyapi]
model = "MODEL_ID_FROM_CATALOG"
base_url = "https://api.spicyapi.ai/v1"
name = "SpicyAPI"
env_key = "SPICY_API_KEY"

[models]
default = "spicyapi"

Zastąp MODEL_ID_FROM_CATALOG dokładnym identyfikatorem modelu tekstowego z katalogu na żywo, w postaci <publisher>/<model>/<task>; nigdy nie zostawiaj symbolu zastępczego. Potem uruchom grok -m spicyapi albo przełącz się w trakcie sesji poleceniem /model spicyapi. Klient posługuje się formatem Chat Completions, więc opisane niżej mapowanie parametrów, zakończenia strumieni i kształty błędów obowiązują bez zmian.

Mapowanie parametrów między protokołami

Pola poszczególnych protokołów są najpierw przekształcane na kanoniczne nazwy parametrów platformy, a potem sprawdzane względem inputSchema wybranego modelu. Parametr, którego model nie obsługuje, zwraca 400; nigdy nie jest po cichu pomijany przy normalnym rozliczeniu żądania. Tabela poniżej pokazuje tylko mapowanie, a „—” oznacza, że protokół nie ma odpowiednika. To, czy dany model przyjmuje parametr i jakie wartości dopuszcza, określa inputSchema tego modelu.

Parametr kanonicznyChat CompletionsResponsesMessagesGemini
messagesmessagesinstructions, inputsystem, messagessystemInstruction, contents
max_tokensmax_tokens, max_completion_tokensmax_output_tokensmax_tokensgenerationConfig.maxOutputTokens
temperaturetemperaturetemperaturetemperaturegenerationConfig.temperature
top_ptop_ptop_ptop_pgenerationConfig.topP
toolstoolstools (typ function)tools (narzędzia własne)tools[].functionDeclarations
tool_choicetool_choicetool_choicetool_choicetoolConfig.functionCallingConfig
response_formatresponse_formattext.format—generationConfig.responseMimeType oraz responseSchema lub responseJsonSchema
reasoning_effortreasoning_effortreasoning.effort—generationConfig.thinkingConfig

Pola, których nie ma w tabeli, również są przekształcane na parametry kanoniczne i sprawdzane przez model; wszystko, czego brakuje w jego inputSchema, zwraca 400. Na przykład tylko niektóre modele wymieniają seed w swoim inputSchema; w pozostałych seed w Chat i generationConfig.seed w Gemini zwracają 400. Wyjątkiem są ustawienia próbkowania i sekwencje stop: w modelu, który ich nie przyjmuje, są ignorowane (wskazuje je nagłówek odpowiedzi X-Spicy-Ignored-Params), a wartość spoza tego, co model przyjmuje, jest zmieniana na najbliższą dozwoloną (wskazuje ją X-Spicy-Adjusted-Params). Dotyczy to temperature, top_p i kar, stop w Chat, stop_sequences w Messages, generationConfig.stopSequences w Gemini oraz poziomów reasoning_effort.

Kilka wyjątków:

  • Pola, które mają znaczenie tylko na platformie danego protokołu, są ignorowane i nie wpływają na generowanie: user, metadata i store w Chat; user, metadata, store i include w Responses; metadata w Messages.
  • Responses nie obsługuje previous_response_id; wysyłaj pełną historię w input.
  • safetySettings w Gemini jest przyjmowane, ale nie ma żadnego skutku. cachedContent i narzędzia po stronie serwera, takie jak googleSearch i codeExecution, nie są obsługiwane i zwracają 400. Obrazy przekazuj jako inlineData (base64) albo fileData.fileUri (adres https); to, czy model przyjmuje obrazy, określa jego inputSchema.

Wyślij strumień Chat

Gdy SPICY_API_KEY i SPICY_MODEL są ustawione jak wyżej, te polecenia zapisują żądanie do pliku i streamują odpowiedź. W razie potrzeby zmień przykładowy prompt, a parametry modelu przed wysłaniem sprawdź względem aktualnego schematu. Każde uruchomienie wysyła świeży Idempotency-Key, bo każde uruchomienie to nowa wiadomość.

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: $(uuidgen)" \
  --data-binary @chat-request.json

--no-buffer wypisuje każdy kawałek zaraz po jego nadejściu, a --max-time 120 przerywa po dwóch minutach. Powinny pojawiać się jedna po drugiej linijki podobne do poniższych, a na końcu data: [DONE]. Odpowiedź jest rozłożona na kawałki delta.content; połącz je po kolei, żeby dostać cały tekst. Ostatni fragment przed [DONE] zawiera usage.

data: {"id":"chatcmpl-req_01k...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"A rainbow"}}]}

data: {"id":"chatcmpl-req_01k...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":" forms when"}}]}

data: [DONE]

uuidgen jest dostępny w macOS; w Linuksie dostarcza go pakiet uuid-runtime.

Wyślij żądanie Gemini

Ten przykład używa tych samych SPICY_API_KEY i SPICY_MODEL i znów nowego Idempotency-Key: to inne żądanie niż w przykładzie Chat, więc ponowne użycie tamtego klucza zwróciłoby 409. Identyfikator modelu trafia do adresu URL, więc treść żądania nie zawiera pola modelu. Ujmij adres URL w podwójny cudzysłów i zapisz zmienną jako ${SPICY_MODEL}, żeby powłoka nie potraktowała następującego po niej dwukropka jako modyfikatora zmiennej.

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: $(uuidgen)" \
  --data-binary @gemini-request.json

Jeśli nie potrzebujesz streamingu, zastąp :streamGenerateContent?alt=sse przez :generateContent i odczytaj odpowiedź z candidates[0].content.parts[].text; zużycie jest w usageMetadata. Bez alt=sse żądanie streamingowe zwraca jedną tablicę JSON zapisywaną przyrostowo; oficjalne SDK dodają alt=sse za ciebie.

Natywny strumień z potwierdzoną ceną

POST /api/v1/jobs/stream to jedyny endpoint tekstowy, który przyjmuje wycenę, więc może odmówić działania, jeśli cena się zmieniła. Treść żądania ma ten sam kształt co w createTask: model oraz input, którego pola pochodzą z inputSchema modelu (dla modeli tekstowych to messages i inne parametry kanoniczne z tabeli wyżej).

# 1. Zapisz żądanie jeden raz.
jq -n --arg model "$SPICY_MODEL" '{
  model: $model,
  input: {messages: [{role: "user", content: "Explain a rainbow in one sentence."}]}
}' > native-request.json

# 2. Pobierz wycenę. Nic nie powstaje i nic nie zostaje pobrane.
QUOTE=$(curl -s https://api.spicyapi.ai/api/v1/jobs/quote \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @native-request.json)
echo "$QUOTE" | jq '{code, msg, estimatedCost: .data.estimatedCost, maxCharge: .data.maxCharge, expiresAt: .data.expiresAt}'

# 3. W ciągu pięciu minut dodaj quoteId i expectedCost do niezmienionego żądania i uruchom strumień.
jq --argjson quote "$QUOTE" \
  '. + {quoteId: $quote.data.quoteId, expectedCost: $quote.data.estimatedCost}' \
  native-request.json > native-confirmed.json

curl --fail-with-body --no-buffer --max-time 120 \
  https://api.spicyapi.ai/api/v1/jobs/stream \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data-binary @native-confirmed.json

Krok 2 wypisuje code: 200 i cenę w dolarach amerykańskich; każdy inny code oznacza, że wycena się nie powiodła, a msg mówi dlaczego. Jeśli wywołanie strumienia nie powiedzie się, zanim zostanie wysłany jakikolwiek tekst – na przykład z kodem 40901, bo zmieniła się cena albo żądanie, albo z kodem 40201 z powodu za niskiego salda – dostajesz zwykły JSON {code,msg,data,request_id} i nic nie zostaje zablokowane. Gdy streaming już ruszył, błąd kończy strumień zdarzeniem event: error z data: {"code":"stream_interrupted"} i nic nie jest za to pobierane. Modele obrazu, wideo i dźwięku zwracają tutaj 503; dla nich użyj createTask.

Odpowiedź przychodzi jako linijki SSE data:. W przeciwieństwie do /v1/chat/completions ten endpoint nie gwarantuje jednego formatu zdarzeń dla wszystkich modeli, więc przetestuj swój parser z wybranym modelem; jeśli potrzebujesz jednego stabilnego formatu, użyj endpointu /v1, a wydatki ogranicz limitami klucza.

Wysyłanie obrazów, wideo i dźwięku do modelu tekstowego

Niektóre modele tekstowe czytają nie tylko tekst, ale i obrazy, a nieliczne także wideo i dźwięk. Wysyłasz multimedia razem z pytaniem, a odpowiedź nadal jest tekstem. To, co model przyjmuje, jest zapisane w jego schemacie, a nie wynika z samego protokołu. Sprawdź wybrany model:

curl -s "https://api.spicyapi.ai/api/v1/models/$SPICY_MODEL" \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  | jq -c '[.data.inputSchema.properties.messages.items.properties.content.items.oneOf[]?.properties.type.const]'

Lista wymienia części treści, które model przyjmuje. ["text","image_url"] oznacza tekst i obrazy; model czytający także wideo i dźwięk wymienia dodatkowo video_url, audio_url oraz input_audio. Pusta lista oznacza wyłącznie tekst. Wysłanie części, której model nie wymienia, zwraca 400 i nic nie kosztuje.

Aby wysłać multimedia, ustaw content wiadomości jako listę części zamiast ciągu znaków: część text z pytaniem i po jednej części na każdy plik:

jq -n --arg model "$SPICY_MODEL" '{
  model: $model,
  stream: true,
  messages: [{
    role: "user",
    content: [
      {type: "text", text: "What is in this picture?"},
      {type: "image_url", image_url: {url: "https://example.com/photo.jpg"}}
    ]
  }]
}' > image-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: $(uuidgen)" \
  --data-binary @image-request.json

Każdy rodzaj multimediów ma własną część:

CzęśćPostaćSkąd pochodzi plik
Obraz{"type": "image_url", "image_url": {"url": "…"}}Publiczny adres HTTPS, wgrany plik (spicy://f/…) lub mały URI data:
Wideo{"type": "video_url", "video_url": {"url": "…"}}Wgrany plik (spicy://f/…) lub publiczny adres HTTPS
Plik audio{"type": "audio_url", "audio_url": {"url": "spicy://f/…"}}Wyłącznie wgrany plik
Dźwięk inline{"type": "input_audio", "input_audio": {"data": "<base64>", "format": "wav"}}Base64 w żądaniu, wav lub mp3

Jak wgrać plik i uzyskać jego identyfikator spicy://f/…, opisano w Mediach; wgrane pliki są przechowywane przez jeden dzień. Z tego, jak każdy rodzaj trafia do modelu, wynika kilka reguł:

  • Obrazy mogą być publicznym adresem HTTPS, który zwraca sam plik obrazu (a nie stronę, na której go wyświetlono), wgranym plikiem albo URI data: w granicach limitów inline. JPEG i PNG działają w każdym modelu, który przyjmuje obrazy.
  • Wideo i dźwięk najlepiej wysyłać jako wgrane pliki. Wgrany plik jest odczytywany i przekazywany modelowi w treści żądania, dlatego wgrane wideo i dźwięk w jednym żądaniu mogą razem mieć co najwyżej 14 MB: wideo MP4 lub WebM, dźwięk MP3 lub WAV. Większa suma zwraca 400 i nic nie kosztuje. Publiczny adres wideo jest przekazywany dalej jako link i działa tylko wtedy, gdy dostawca modelu może go pobrać.
  • Dźwięk inline (input_audio) jest zgodny z formatem OpenAI i musi się zmieścić w treści żądania o rozmiarze 2 MiB, więc nadaje się do krótkich nagrań; dłuższy dźwięk wgraj i wyślij jako audio_url.

W jednej wiadomości można wysłać kilka plików, każdy jako osobna część, a wcześniejsze wiadomości w historii również mogą zawierać multimedia.

Pozostałe protokoły przenoszą obrazy w swojej własnej postaci i wszystkie docierają do modelu identycznie. Części z wideo i dźwiękiem są wysyłane przez Chat Completions i protokół natywny.

ProtokółCzęść z obrazem
Chat Completions{"type": "image_url", "image_url": {"url": "…"}}
Responses{"type": "input_image", "image_url": "…"}
Anthropic Messages{"type": "image", "source": {"type": "url", "url": "…"}}, lub źródło base64
GeminiinlineData (base64) lub fileData.fileUri (adres HTTPS)

Ile to kosztuje. Multimedia są liczone jako tokeny wejściowe według zwykłej stawki wejściowej modelu; nie ma osobnej ceny za plik. Obraz zwykle dodaje od kilkuset do kilku tysięcy tokenów, zależnie od modelu i rozmiaru obrazu; wideo i dźwięk rosną wraz z długością. Dokładną liczbę podaje blok usage w odpowiedzi.

Co jest blokowane z góry. Przed wykonaniem żądania blokowana jest górna granica kwoty, a różnica jest zwalniana po rozliczeniu. W żądaniu, w którym jedynymi multimediami są obrazy przesłane przez Ciebie lub wysłane w treści żądania, granica dolicza stały zapas na każdy obraz. W żądaniu zawierającym wideo lub dźwięk albo obraz podany jako publiczny adres URL granica jest ustalana według całego limitu wejścia modelu, ponieważ jego rozmiaru nie da się poznać z góry; zablokowana kwota może więc być znacznie wyższa niż końcowa opłata. Prześlij obrazy, aby blokada była niewielka, utrzymuj saldo wystarczające na blokadę albo najpierw wyceń żądanie przez POST /api/v1/jobs/quote, aby ją zobaczyć.

W Playground. W modelu przyjmującym multimedia pole wiadomości pokazuje przycisk załączania. Można też wkleić zrzut ekranu albo upuścić pliki na pole; jedna wiadomość przyjmuje do czterech załączników, a wideo i dźwięk wśród nich mogą razem mieć 14 MB. Modele czytające tylko tekst nie pokazują tego przycisku.

Rozmowy i wywołania narzędzi

Chat Completions

  • Bez streamingu: czytaj choices[0].message.
  • Ze streamingiem: przetwarzaj choices[].delta zdarzenie SSE po zdarzeniu, a nie pakiet sieciowy po pakiecie. Składaj tool_calls według index, a wywołanie sprawdzaj i wykonuj dopiero wtedy, gdy jego arguments są kompletne.
  • Zachowaj wiadomość asystenta w historii, a każdy wynik zwracaj w wiadomości tool z pasującym tool_call_id.

Responses

  • Wysyłaj pełną historię w input; previous_response_id jest odrzucane.
  • Wyniki funkcji zwracaj jako elementy function_call_output z pasującym call_id.

Messages

  • Używaj bloków treści text, tool_use i tool_result zamiast kształtu wiadomości znanego z Chat.

Gemini

  • contents przeplatają dwie role: user i model.
  • Gdy model chce wywołać narzędzie, zwraca część functionCall. Po uruchomieniu narzędzia opakuj wynik w część functionResponse i odeślij go w kolejnej treści user, z tą samą wartością name co w wywołaniu.

Zakończenie, błędy i rozłączenia

ProtokółNormalne zakończenieZużycie
Chat Completionsdata: [DONE]usage w ostatnim fragmencie
ResponsesZdarzenie response.completedresponse.usage w tym zdarzeniu
Messagesmessage_stop po message_deltausage w message_delta
GeminiOstatnie zdarzenie z finishReason; nie ma [DONE]usageMetadata

Zanim zaczniesz czytać strumień, sprawdź status HTTP i Content-Type; błędy wracają jako JSON w formacie danego protokołu:

ProtokółTreść błędu
Chat Completions, Responses{"error": {"message", "type", "param", "code"}}
Messages{"type": "error", "error": {"type", "message"}}
Gemini{"error": {"code", "message", "status"}}

Kody statusu HTTP są takie same we wszystkich czterech protokołach. status w Gemini używa nazw statusów Google: 400 to INVALID_ARGUMENT, 401 to UNAUTHENTICATED, 402 to FAILED_PRECONDITION, 403 to PERMISSION_DENIED, 404 to NOT_FOUND, 409 to ABORTED, 429 to RESOURCE_EXHAUSTED, 500 to INTERNAL, 503 to UNAVAILABLE, a 504 to DEADLINE_EXCEEDED. Logikę kodu rozgałęziaj po kodzie statusu i typie błędu, nigdy po treści komunikatu.

Jeśli coś się nie powiedzie po rozpoczęciu streamingu, Chat i Gemini wysyłają zdarzenie z obiektem błędu, Responses wysyła zdarzenie błędu, a Messages – event: error. Twój klient musi obsługiwać te zdarzenia błędów, a także przekroczenia czasu i połączenie zamknięte bez normalnego znacznika zakończenia. Jeśli przepuszczasz strumień przez własne proxy, wyłącz w nim buforowanie odpowiedzi, bo inaczej kawałki będą przychodzić seriami.

Ile płacisz, gdy strumień urwie się przedwcześnie

  • Rozłączasz się (zamykasz kartę, przerywasz żądanie, klient przekracza limit czasu): rozłączenie nie zatrzymuje naliczania. Płacisz za zużycie zgłoszone przez model, które w przypadku wielu modeli obejmuje całą odpowiedź – łącznie z tekstem, który do ciebie nie dotarł – i nigdy nie przekracza kwoty zablokowanej przy przyjęciu żądania. Tekstu, który nie dotarł, nie da się później pobrać. Żeby wydać mniej, obniż max_tokens, zamiast się rozłączać.
  • Model albo nasza usługa zawodzi w trakcie strumienia (dostajesz zdarzenie błędu albo strumień kończy się bez znacznika zakończenia): żądanie zostaje oznaczone jako nieudane i nic nie jest pobierane.
  • Model wysłał ci tekst lub rozumowanie, a potem jego odpowiedź została uznana za pustą albo za odmowę: to, co już do ciebie dotarło, jest rozliczane według zużycia zgłoszonego przez model, nigdy powyżej zablokowanej kwoty, a strumień kończy się normalnie. Pełny zwrot przysługuje tylko wtedy, gdy nic do ciebie nie dotarło.
  • Żądanie przekracza termin po stronie serwera: kończy się niepowodzeniem i nic nie jest pobierane.

Limity czasu i streaming

Bez streamingu odpowiedź przychodzi w całości, gdy jest gotowa, a długie odpowiedzi i długie rozumowanie wymagają czasu: modele takie jak Grok, GLM, DeepSeek Pro czy Claude Opus potrafią myśleć kilka minut, zanim cokolwiek napiszą, zwłaszcza przy wysokim reasoning_effort. Brzeg sieci przed API zamyka połączenie, przez które przez około 100 sekund nic nie dotarło, dlatego żądania obu rodzajów są utrzymywane otwarte, dopóki model pracuje.

  • Żądanie bez streamingu, które po około 30 sekundach wciąż trwa, zaczyna odpowiedź wcześniej. Dostajesz status 200, Content-Type: application/json i nagłówek X-Spicy-Keepalive: 1, potem co 15 sekund znak nowej linii, a na końcu treść JSON. JSON dopuszcza białe znaki przed wartością, więc standardowe parsery (json.loads, JSON.parse, response.json()) i oficjalne SDK czytają treść bez zmian. Żądanie, które skończy się wcześniej, dostaje zwykłą odpowiedź z prawdziwym kodem statusu i bez tego nagłówka.
  • Od tej chwili błąd jest w treści, nie w statusie. Status 200 został już wysłany, więc treść zawiera zwykły obiekt błędu danego protokołu (z tabeli powyżej), z tym samym kodem i tym samym komunikatem; w formacie Gemini error.code nadal zawiera status HTTP, jaki miałby ten błąd. Gdy jest nagłówek X-Spicy-Keepalive, przed odczytaniem odpowiedzi sprawdź, czy treść nie zawiera obiektu error: oficjalne SDK nie traktują 200 jako błędu. Za nieudane żądanie nie pobiera się opłaty.
  • Streaming także utrzymuje połączenie otwarte. Dopóki model jeszcze pracuje, odpowiedź strumieniowa przenosi linie keep-alive (komentarze : keep-alive w SSE, puste linie w formacie Gemini), więc połączenie nigdy nie jest bezczynne, niezależnie od tego, jak długo model myśli. Twój parser musi je pomijać, jak wymaga tego format SSE; oficjalne SDK już to robią.
  • We wszystkim, co może trwać długo, lepiej używaj stream: true. Widzisz odpowiedź na bieżąco, w miarę jak powstaje, a błąd przychodzi jako zdarzenie błędu, które SDK rozumieją. Chat Completions, Responses i Messages przyjmują stream: true; w formacie Gemini wywołuj :streamGenerateContent?alt=sse zamiast :generateContent. Zmienione body to nowe żądanie, więc wyślij je z nowym Idempotency-Key: ponowne użycie starego zwraca 409.
  • Za żądanie przerwane przez brzeg sieci nie pobiera się opłaty. Jeśli przez około 100 sekund nic do ciebie nie dotrze i połączenie zostanie zamknięte, żądanie widać w Konsola → Logi i w GET /api/v1/jobs jako nieudane, z errorCode timeout i komunikatem wyjaśniającym, co się stało. Jeśli wyślesz je ponownie z tym samym Idempotency-Key, dostaniesz ten komunikat jako błąd.
  • Twoje własne limity czasu nadal obowiązują. Całkowity limit czasu w twoim kliencie HTTP albo w proxy, którym zarządzasz, kończy także żądanie, a gdy cokolwiek już do ciebie dotarło (również linia keep-alive), liczy się to jako rozłączenie z twojej strony (zobacz Ile płacisz, gdy strumień urwie się przedwcześnie). Oficjalne SDK OpenAI domyślnie czekają do 10 minut; przykłady z curl na tej stronie poddają się po 2 minutach, więc przy długim rozumowaniu zwiększ --max-time.

Tak włączysz streaming w SDK OpenAI i w curl:

OpenAI Python
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
stream = client.chat.completions.create(
    model=os.environ["SPICY_MODEL"],
    messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
    stream=True,
)
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
print()
OpenAI Node.js
import OpenAI from "openai";

const client = new OpenAI({ apiKey: process.env.SPICY_API_KEY, baseURL: "https://api.spicyapi.ai/v1" });
const stream = await client.chat.completions.create({
  model: process.env.SPICY_MODEL,
  messages: [{ role: "user", content: "Explain a rainbow in one sentence." }],
  stream: true,
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
process.stdout.write("\n");
curl
curl --fail-with-body --no-buffer --max-time 900 \
  https://api.spicyapi.ai/v1/chat/completions \
  -H "Authorization: Bearer $SPICY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data-binary @chat-request.json

Przykład dla Node.js używa await na najwyższym poziomie, więc uruchom go jako moduł ES (na przykład zapisz go jako stream.mjs). Polecenie curl wysyła chat-request.json z sekcji Wyślij strumień Chat, który już ustawia stream: true; z --max-time 900 odpowiedź może trwać do 15 minut.

Wyceny i rozliczenia

Jeśli potrzebujesz wyraźnego potwierdzenia kosztu, użyj natywnego strumienia z wyceną. /v1 i /v1beta nie przyjmują danych wyceny; stosują cenę z chwili przyjęcia.

Wywołania tekstowe nie zwracają identyfikatora zadania, a znacznik końca tekstu nie dowodzi ostatecznej opłaty. Żeby sprawdzić, ile kosztowało wywołanie, wyświetl listę ostatnich zadań tym samym kluczem API – GET /api/v1/jobs?model=<model>&from=<YYYY-MM-DD> – i dopasuj je po createdAt albo zajrzyj do Konsola → Logi. cost traktuj jako ostateczny dopiero wtedy, gdy settled ma wartość true. Zobacz Historia zadań i odzyskiwanie oraz Rozliczenia.

reasoning_tokens są już wliczone w completion_tokens: nie dodawaj ich drugi raz.

Inne kształty żądań

Te przykłady opisują wyłącznie struktury protokołów. Zastąp MODEL_ID_FROM_CATALOG modelem z katalogu na żywo, który obsługuje pokazane pola; nigdy nie wysyłaj samego symbolu zastępczego.

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
}

Czytaj dalej

Wyceny i zgodność protokołów · Idempotentność · Rozliczenia · Integracja produkcyjna · Rozwiązywanie problemów i odzyskiwanie

Na tej stronie