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żejMODEL_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ół | Endpoint | Nagłówek z kluczem API |
|---|---|---|
| OpenAI Chat Completions | POST /v1/chat/completions | Authorization: Bearer |
| OpenAI Responses | POST /v1/responses | Authorization: Bearer |
| Anthropic Messages | POST /v1/messages | x-api-key lub Authorization: Bearer |
| Google Gemini | POST /v1beta/models/{model}:generateContent | x-goog-api-key lub Authorization: Bearer |
| Google Gemini (streaming) | POST /v1beta/models/{model}:streamGenerateContent | x-goog-api-key lub Authorization: Bearer |
| Natywny strumień z potwierdzoną ceną | POST /api/v1/jobs/stream | Authorization: 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.
| SDK | Adres 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.
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)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)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ślijPOST /api/v1/jobs/streamze zwróconymiquoteIdiexpectedCost. Jeśli cena w międzyczasie się zmieniła, przyjęcie nie udaje się z kodem40901, 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
40202zamiast 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:
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.
[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 kanoniczny | Chat Completions | Responses | Messages | Gemini |
|---|---|---|---|---|
messages | messages | instructions, input | system, messages | systemInstruction, contents |
max_tokens | max_tokens, max_completion_tokens | max_output_tokens | max_tokens | generationConfig.maxOutputTokens |
temperature | temperature | temperature | temperature | generationConfig.temperature |
top_p | top_p | top_p | top_p | generationConfig.topP |
tools | tools | tools (typ function) | tools (narzędzia własne) | tools[].functionDeclarations |
tool_choice | tool_choice | tool_choice | tool_choice | toolConfig.functionCallingConfig |
response_format | response_format | text.format | — | generationConfig.responseMimeType oraz responseSchema lub responseJsonSchema |
reasoning_effort | reasoning_effort | reasoning.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,metadataistorew Chat;user,metadata,storeiincludew Responses;metadataw Messages. - Responses nie obsługuje
previous_response_id; wysyłaj pełną historię winput. safetySettingsw Gemini jest przyjmowane, ale nie ma żadnego skutku.cachedContenti narzędzia po stronie serwera, takie jakgoogleSearchicodeExecution, nie są obsługiwane i zwracają400. Obrazy przekazuj jakoinlineData(base64) albofileData.fileUri(adres https); to, czy model przyjmuje obrazy, określa jegoinputSchema.
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.jsonJeś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.jsonKrok 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.jsonKaż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
400i 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 jakoaudio_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 |
| Gemini | inlineData (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[].deltazdarzenie SSE po zdarzeniu, a nie pakiet sieciowy po pakiecie. Składajtool_callswedługindex, a wywołanie sprawdzaj i wykonuj dopiero wtedy, gdy jegoargumentssą kompletne. - Zachowaj wiadomość asystenta w historii, a każdy wynik zwracaj w wiadomości
toolz pasującymtool_call_id.
Responses
- Wysyłaj pełną historię w
input;previous_response_idjest odrzucane. - Wyniki funkcji zwracaj jako elementy
function_call_outputz pasującymcall_id.
Messages
- Używaj bloków treści
text,tool_useitool_resultzamiast kształtu wiadomości znanego z Chat.
Gemini
contentsprzeplatają dwie role:userimodel.- Gdy model chce wywołać narzędzie, zwraca część
functionCall. Po uruchomieniu narzędzia opakuj wynik w częśćfunctionResponsei odeślij go w kolejnej treściuser, z tą samą wartościąnameco w wywołaniu.
Zakończenie, błędy i rozłączenia
| Protokół | Normalne zakończenie | Zużycie |
|---|---|---|
| Chat Completions | data: [DONE] | usage w ostatnim fragmencie |
| Responses | Zdarzenie response.completed | response.usage w tym zdarzeniu |
| Messages | message_stop po message_delta | usage w message_delta |
| Gemini | Ostatnie 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/jsoni nagłówekX-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
200został 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 Geminierror.codenadal zawiera status HTTP, jaki miałby ten błąd. Gdy jest nagłówekX-Spicy-Keepalive, przed odczytaniem odpowiedzi sprawdź, czy treść nie zawiera obiektuerror: oficjalne SDK nie traktują200jako 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-alivew 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=ssezamiast:generateContent. Zmienione body to nowe żądanie, więc wyślij je z nowymIdempotency-Key: ponowne użycie starego zwraca409. - 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/jobsjako nieudane, zerrorCodetimeouti komunikatem wyjaśniającym, co się stało. Jeśli wyślesz je ponownie z tym samymIdempotency-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:
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()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 --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.jsonPrzykł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.
{
"model": "MODEL_ID_FROM_CATALOG",
"input": "Explain a rainbow in one sentence.",
"stream": false
}{
"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
Wyceny i zgodność protokołów
Poznaj dokładną cenę żądania przed zapłatą, wyślij je po tej cenie, odzyskaj utraconą odpowiedź i sprawdź, jak wyceniane są zgodne API dla tekstu i wideo.
Przykłady czatu
Kompletne działające programy do rozmów wieloturowych, roleplay z postacią, strumieniowania do własnego interfejsu i wywoływania narzędzi w Python, Node.js i curl.

