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.
Każdy przykład na tej stronie jest pełnym programem: ustaw dwie zmienne środowiskowe, skopiuj go i uruchom. Wszystkie wywołują POST https://api.spicyapi.ai/v1/chat/completions przez oficjalne biblioteki OpenAI, więc ten sam kod działa z każdym modelem tekstowym w katalogu. Tekst i strumieniowanie wyjaśnia sam protokół; ta strona pokazuje, jak zbudować na nim typowe funkcje.
Zanim zaczniesz
1. Umieść klucz API w zmiennej środowiskowej, aby nigdy nie wklejać go do kodu:
export SPICY_API_KEY="sk-spicy-paste-your-key-here"2. Wybierz model tekstowy z katalogu na żywo i zachowaj jego identyfikator w SPICY_MODEL. Każdy przykład go odczytuje. To polecenie wymienia modele tekstowe dostępne teraz dla twojego klucza (wymaga jq):
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'
export SPICY_MODEL="MODEL_ID_FROM_CATALOG" # replace with one line from the list aboveIdentyfikatory modeli tekstowych mają postać publisher/model/chat. MODEL_ID_FROM_CATALOG to tylko symbol zastępczy: nigdy nie wysyłaj go dosłownie.
3. Sprawdź, co przyjmuje dany model. Modele się różnią: niektóre obsługują temperature, inne nie; reasoning_effort ma różne wartości w różnych modelach; tylko część przyjmuje obrazy. Nieobsługiwane pola próbkowania, takie jak temperature lub frequency_penalty, są ignorowane i wymieniane w nagłówku odpowiedzi X-Spicy-Ignored-Params. Obsługiwane są też typowe wartości domyślne klientów: nieobsługiwane stop jest ignorowane, nieobsługiwane n: 1 oznacza jedną odpowiedź, a rozpoznany poziom rozumowania jest w razie potrzeby mapowany na najbliższy obsługiwany. Nagłówek X-Spicy-Adjusted-Params pokazuje dostosowane ustawienia. Nieobsługiwane pola funkcjonalne, takie jak tools lub response_format, nadal powodują 400. To polecenie wypisuje pola, wartości reasoning_effort i rodzaje treści wiadomości przyjmowane przez model:
curl -s "https://api.spicyapi.ai/api/v1/models/$SPICY_MODEL" \
-H "Authorization: Bearer $SPICY_API_KEY" \
| jq '.data.inputSchema.properties | {
fields: keys,
reasoning_effort: .reasoning_effort.enum,
content_parts: [.messages.items.properties.content.items.oneOf[]?.properties.type.const]
}'Dla szybkiego przeglądu wszystkich modeli tekstowych naraz GET /v1/models?modality=text zwraca supported_parameters, context_length i max_output_tokens każdego modelu; wymienia tylko nazwy pól, więc do sprawdzenia dozwolonych wartości użyj powyższego polecenia.
4. Zainstaluj bibliotekę OpenAI dla swojego języka: pip install openai dla Pythona lub npm install openai dla Node.js 18 albo nowszego. Programy Node.js zapisz z rozszerzeniem .mjs (używają await na najwyższym poziomie) i uruchom przez node file.mjs.
Zostaw miejsce na rozumowanie
Wiele modeli tekstowych rozumuje przed odpowiedzią, a to rozumowanie wlicza się do max_tokens. Przykłady pomijają max_tokens, więc każdy model korzysta z własnej wartości domyślnej. Jeśli je ustawiasz, daj odpowiedni zapas (kilka tysięcy tokenów): zbyt mała wartość może zostać w całości zużyta na rozumowanie, a wtedy żądanie nie powiedzie się z komunikatem zalecającym zwiększenie max_tokens lub zmniejszenie reasoning_effort. Takie niepowodzenie nie jest płatne.
Rozmowa wieloturowa
API nie zachowuje pamięci między wywołaniami. Rozmowa to lista wiadomości, którą ty przechowujesz i ponownie wysyłasz w całości przy każdej turze: najpierw wiadomość system, potem naprzemienne wiadomości user i assistant. Po każdej odpowiedzi dodaj ją do listy jako wiadomość assistant.
Każda ponownie wysłana wiadomość jest znów rozliczana jako wejście, więc usuwaj najstarsze tury, gdy rozmowa się wydłuża. Zawsze zachowuj wiadomość system i usuwaj tury parami użytkownik/asystent, aby historia nadal zaczynała się od wiadomości user.
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
MODEL = os.environ["SPICY_MODEL"]
KEEP = 20 # messages kept after the system prompt; an even number keeps user/assistant pairs together
history = [{"role": "system", "content": "You are a patient cooking coach. Keep every answer under 80 words."}]
while True:
try:
user_text = input("you> ").strip()
except EOFError: # Ctrl-D
break
if user_text in ("", "quit", "exit"):
break
history.append({"role": "user", "content": user_text})
reply = client.chat.completions.create(model=MODEL, messages=history)
answer = reply.choices[0].message.content or ""
print(f"coach> {answer}\n")
history.append({"role": "assistant", "content": answer})
# The API remembers nothing between calls: resend the conversation every turn,
# and trim it so the prompt (and what you pay for it) stops growing.
history = history[:1] + history[1:][-KEEP:]Model odpowiada na ostatnią wiadomość, korzystając ze wszystkiego, co ją poprzedza: zapytaj go później, jakie składniki wymieniono, a będzie wiedział. Modele rozumujące zwracają też reasoning_content obok content; przy zwykłej rozmowie nie musisz wysyłać go z powrotem.
Roleplay z kartą postaci
Postać to starannie napisana wiadomość system. Ponieważ API nie zachowuje pamięci, karta postaci towarzyszy każdemu żądaniu — to właśnie pomaga zachować tę samą postać również w pięćdziesiątej turze. Dobra karta odpowiada na sześć pytań:
| Część | Co napisać | Przykład |
|---|---|---|
| Tożsamość | Kim jest postać, w dwóch lub trzech faktach kształtujących zachowanie | Emerytowany detektyw, który teraz opiekuje się latarnią morską |
| Głos | Jak mówi: długość zdań, słownictwo, nawyki, sposób zwracania się do użytkownika | Spokojny, głośno zauważa drobne szczegóły, mówi do użytkownika „przyjacielu” |
| Świat | Miejsce akcji i jeden lub dwa fakty napędzające historię | Miasteczko rybackie w latach 20. XX wieku, gdzie o północy dzwoni dzwon zatopionego statku |
| Granice | Co postać zrobi, czego nie zrobi i jak wyjść poza opowieść | Pozostaje w świecie historii; krótko odpowiada poza rolą na (OOC: ...); utrzymuje historię odpowiednią dla ogólnej publiczności |
| Narracja | Osoba i czas narracji oraz oznaczenie wypowiedzi | Trzecia osoba, czas przeszły; wypowiedzi w cudzysłowie |
| Format | Długość i zakończenie każdej odpowiedzi | Najwyżej 120 słów, na końcu dwie lub trzy ponumerowane opcje |
Dwa nawyki pomagają stworzyć naturalną sesję roleplay. Dodaj pierwszą wypowiedź postaci do historii jako wiadomość assistant, aby użytkownik od początku widział ją w roli. Każdą odpowiedź kończ pytaniem lub wyborem, aby zawsze było wiadomo, jak kontynuować; w interaktywnej fikcji numerowane opcje pozwalają odpowiedzieć jedną cyfrą.
Nawigator sterowca w świecie fantasy. Odpowiedź jest strumieniowana słowo po słowie.
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
MODEL = os.environ["SPICY_MODEL"]
CARD = {
"name": "Captain Ilsa Varn",
"identity": "Navigator of the airship Kestrel. Forty years old, calm under pressure, dry sense of humour, fiercely loyal to her crew.",
"voice": "Short, vivid sentences full of wind and weather. Calls the user 'deckhand'. No modern slang.",
"world": "Islands drift on warm updrafts above an endless sea of cloud. Storms here are alive and hungry, and maps go out of date within a week.",
"boundaries": (
"Stay in character and inside this world. If the user asks about the real world, react as Ilsa would: puzzled but polite. "
"If the user writes (OOC: ...), step out of the story for one short, plain answer, then return. "
"Keep the story suitable for a general audience."
),
"narration": "Describe the scene in the second person ('You feel the deck tilt...'). Put Ilsa's spoken lines in quotation marks.",
"format": "At most 120 words. End every reply with a question or a choice, so the user always knows how to continue.",
}
system_prompt = "\n".join(
[f"You are {CARD['name']}, a character in an interactive story."]
+ [f"{key.capitalize()}: {value}" for key, value in CARD.items() if key != "name"]
)
history = [
{"role": "system", "content": system_prompt},
{"role": "assistant", "content": "\"Welcome aboard, deckhand. Storm wall to the north, a lost island to the east. Which way do we steer?\""},
]
print(history[-1]["content"])
while True:
try:
user_text = input("\nyou> ").strip()
except EOFError: # Ctrl-D
break
if user_text in ("", "quit", "exit"):
break
history.append({"role": "user", "content": user_text})
answer = ""
stream = client.chat.completions.create(model=MODEL, messages=history, stream=True)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
piece = chunk.choices[0].delta.content
answer += piece
print(piece, end="", flush=True)
print()
history.append({"role": "assistant", "content": answer})Te same sześć części sprawdza się w każdym gatunku. Komputer statku science fiction, towarzysz pomagający w codziennej rutynie lub narrator przygodowej gry tekstowej wymagają tylko innej karty:
You are Juniper, a friendly running coach inside a fitness app.
Identity: a former trail runner, upbeat and practical, never judgemental.
Voice: warm and brief; uses the user's name when they give it; celebrates small wins.
World: the user's own week. Remember goals and aches they mention earlier in this conversation.
Boundaries: give general fitness encouragement only; for pain, injury or medical questions, suggest seeing a professional. Stay in character; answer (OOC: ...) plainly.
Narration: first person, present tense, talking directly to the user.
Format: at most 80 words; end with one concrete suggestion for today.Dla długich historii utrzymuj kartę krótką i stabilną, a gdy historia rozmowy rośnie, zastępuj najstarsze tury krótką notatką „Dotychczasowa historia” w drugiej wiadomości system, zamiast pozwalać promptowi rosnąć bez ograniczeń.
Strumieniowanie do własnego interfejsu
Strumieniowanie pokazuje odpowiedź podczas jej powstawania, dzięki czemu interfejs czatu wydaje się responsywny. Sześć zasad obejmuje każdy model w katalogu:
- Poproś o dane użycia, jeśli chcesz liczbę tokenów. Strumienie przestrzegają zasad OpenAI: bez
stream_options.include_usageżaden fragment nie zawierausage. Z"include_usage": truetuż przed[DONE]przychodzi dodatkowy fragment z ostatecznymusagei pustą listąchoices, a każdy pozostały mausage: null. - Sprawdź
choicesprzed odczytaniemchoices[0]. Każdy fragment poza końcowym fragmentem użycia ma co najmniej jeden wybór, więc jest to istotne tylko przy żądaniu danych użycia, ale sprawdzenie nic nie kosztuje i chroni pętlę w obu przypadkach. - Rozumowanie jest osobne. Modele rozumujące strumieniują rozumowanie w
delta.reasoning_content, a odpowiedź wdelta.content. Pierwsze pokazuj przygaszone, zwinięte lub wcale; tylko drugie stanowi odpowiedź. finish_reason: "length"oznacza ucięcie. Model osiągnąłmax_tokens. Zwiększ limit lub zmniejszreasoning_effort.- Błędy mogą dotrzeć w trakcie strumienia. Niepowodzenie po rozpoczęciu strumieniowania przychodzi jako zdarzenie z obiektem
error, a biblioteki OpenAI zgłaszają je jako wyjątek. Takie nieudane żądanie nie jest płatne. Jeśli model ukończył już odpowiedź, zanim zerwało się jego połączenie, nie otrzymasz żadnego błędu: strumień kończy się normalnie i odpowiedź jest rozliczana jak każda inna. Szczegóły: Zakończenie, błędy i zerwane połączenia. - Wcześniejsze przerwanie nie zatrzymuje naliczania. Zamknięcie połączenia nie zatrzymuje modelu; płacisz za użycie, które zgłosi. Zamiast tego obniż
max_tokens(Opłata po wcześniejszym zakończeniu strumienia).
Renderer terminalowy przestrzegający wszystkich sześciu zasad: przygaszone rozumowanie, odpowiedź w pełnym kolorze i końcowy wiersz z liczbami tokenów.
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": "Write a four-line poem about a cat who lives in a lighthouse."}],
stream=True,
stream_options={"include_usage": True},
)
finish_reason = None
usage = None
thinking_shown = False
for chunk in stream:
# With include_usage, only the last chunk before [DONE] carries usage; it has no choices.
if chunk.usage:
usage = chunk.usage
if not chunk.choices:
continue
choice = chunk.choices[0]
thinking = getattr(choice.delta, "reasoning_content", None) # reasoning models only
if thinking:
thinking_shown = True
print(f"\033[2m{thinking}\033[0m", end="", flush=True) # dimmed
if choice.delta.content:
if thinking_shown:
print("\n")
thinking_shown = False
print(choice.delta.content, end="", flush=True)
if choice.finish_reason:
finish_reason = choice.finish_reason
print()
if finish_reason == "length":
print("[stopped at max_tokens: raise it, or lower reasoning_effort]")
if usage:
print(f"[{usage.prompt_tokens} prompt tokens, {usage.completion_tokens} completion tokens]")Wywoływanie narzędzi
Narzędzia pozwalają modelowi poprosić twój program o działanie (rzut kośćmi, wyszukanie zamówienia, odczyt kalendarza), a następnie odpowiedzieć na podstawie wyniku. Pętla zawsze wygląda tak samo:
- Wyślij wiadomości razem z
tools, listą funkcji opisanych za pomocą JSON Schema. - Jeśli odpowiedź ma
tool_calls, dodaj tę wiadomość asystenta dokładnie w otrzymanej postaci, samodzielnie wykonaj każde wywołanie i dodaj jedną wiadomośćtoolna wywołanie z pasującymtool_call_id. - Wyślij wszystko ponownie. Powtarzaj, aż odpowiedź nie będzie miała
tool_calls: właśnie ta odpowiedź jest wynikiem.
Traktuj argumenty jako niezaufane dane wejściowe: parsuj i sprawdzaj je, a zamiast powodować awarię zwracaj błąd jako wynik narzędzia. Ogranicz liczbę rund, aby zdezorientowany model nie mógł zapętlić się bez końca. Każdy model tekstowy w katalogu przyjmuje obecnie tools; sprawdź wartości tool_choice przed wymuszeniem konkretnego narzędzia, ponieważ niektóre modele przyjmują tylko auto i none.
import json
import os
import random
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
MODEL = os.environ["SPICY_MODEL"]
def roll_dice(count, sides):
rolls = [random.randint(1, sides) for _ in range(count)]
return {"rolls": rolls, "total": sum(rolls)}
LOCAL_TOOLS = {"roll_dice": roll_dice}
TOOLS = [{
"type": "function",
"function": {
"name": "roll_dice",
"description": "Roll dice for a tabletop game and return every roll and the total.",
"parameters": {
"type": "object",
"properties": {
"count": {"type": "integer", "minimum": 1, "maximum": 10, "description": "How many dice to roll"},
"sides": {"type": "integer", "enum": [4, 6, 8, 10, 12, 20], "description": "Sides on each die"},
},
"required": ["count", "sides"],
},
},
}]
messages = [
{"role": "system", "content": "You are the game master of a fantasy adventure. Whenever the rules call for a roll, use roll_dice; never invent a result. A roll of 12 or more on a d20 succeeds."},
{"role": "user", "content": "I try to pick the lock on the old tower door. Roll a d20 for me."},
]
for _ in range(5): # a hard stop, so a confused model cannot loop forever
reply = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS)
message = reply.choices[0].message
# Send the assistant turn back exactly as it came, tool calls included.
messages.append(message.model_dump(exclude_none=True))
if not message.tool_calls:
print(message.content)
break
for call in message.tool_calls:
tool = LOCAL_TOOLS.get(call.function.name)
try:
result = tool(**json.loads(call.function.arguments or "{}")) if tool else {"error": "unknown tool"}
except (TypeError, ValueError) as error: # the model sent arguments the tool cannot use
result = {"error": str(error)}
print(f"[tool] {call.function.name}({call.function.arguments}) -> {result}")
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})Pierwsza odpowiedź kończy się finish_reason: "tool_calls", a content ma wartość null; ostatnia kończy się "stop". Modele rozumujące mogą też zawierać reasoning_content w turze asystenta: zachowaj je przy ponownym wysyłaniu tej tury, jak w przykładach dodających wiadomość bez zmian. Przy strumieniowaniu odpowiedzi wywołującej narzędzie wywołanie dociera w częściach; łącz je według index, zgodnie z Rozmowami i wywołaniami narzędzi.
Wynik JSON
Gdy program potrzebuje danych zamiast tekstu, poproś o JSON przez response_format w modelach, których inputSchema wymienia to pole, i opisz pola w prompcie. Zawsze ostrożnie parsuj odpowiedź: odpowiedź ucięta przez max_tokens nie jest prawidłowym dokumentem JSON.
import json
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": "system", "content": "Reply with a JSON object only, with the keys title (string), genre (string) and hook (one sentence)."},
{"role": "user", "content": "Invent a cosy mystery novel set in a mountain bakery."},
],
response_format={"type": "json_object"},
)
try:
book = json.loads(reply.choices[0].message.content)
print(book["title"], "-", book["hook"])
except (json.JSONDecodeError, KeyError, TypeError):
print("Not the JSON we asked for:", reply.choices[0].message.content)W modelu niewymieniającym response_format pomiń to pole i polegaj wyłącznie na prompcie systemowym; powyższy blok try jest wtedy jeszcze ważniejszy.
Gdy coś się nie powiedzie
Błędy wracają w strukturze OpenAI {"error": {"message", "type", "param", "code"}}, więc biblioteki OpenAI zgłaszają standardowe wyjątki. Logikę opieraj na statusie HTTP i error.code; pokazuj ludziom error.message, ponieważ wskazuje potrzebną zmianę. Gdy błąd dotyczy jednego pola, param podaje jego nazwę jako ścieżkę z kropkami, na przykład temperature lub messages.1.content.0.
| Status | error.code | Co się stało | Co zrobić |
|---|---|---|---|
| 400 | unsupported_parameter | Model nie przyjmuje pola (na przykład logprobs: is not a supported field) lub tego rodzaju części wiadomości (na przykład części image_url w modelu wyłącznie tekstowym). Pola próbkowania, takie jak temperature, nie są odrzucane; są ignorowane i wymieniane w X-Spicy-Ignored-Params | Usuń je. Sprawdź pola modelu jak w Zanim zaczniesz |
| 400 | invalid_request | Brakuje pola lub ma ono nieakceptowaną wartość, na przykład reasoning_effort: must be one of: "low", "high", "max"; albo model nie jest modelem czatu (param to model) | Zmień wartość wskazaną w komunikacie lub wybierz model z identyfikatorem kończącym się na /chat |
| 400 | content_rejected | Zasady treści modelu spowodowały odmowę | Przeformułuj żądanie; ponowne wysłanie tych samych danych znów spowoduje odmowę |
| 401 | invalid_api_key | Brak klucza, literówka lub unieważniony klucz | Sprawdź SPICY_API_KEY; klucze zaczynają się od sk-spicy- |
| 402 | insufficient_balance, spend_cap_exceeded | Saldo lub limit wydatków nie pokrywa kwoty blokowanej na to żądanie | Dodaj środki, podnieś limit lub obniż max_tokens (poniżej) |
| 404 | model_not_found | Żaden model nie ma tego identyfikatora (param to model). Komunikat pokazuje jego format i, jeśli taki istnieje, najbliższy model czatu dostępny do wywołania | Skopiuj dokładny identyfikator z katalogu na żywo |
| 404 | not_found | Model istnieje, ale teraz nie można go wywołać | Wybierz inny model z katalogu na żywo |
| 409 | conflict | Idempotency-Key użyto ponownie z inną treścią żądania | Każdą nową wiadomość wysyłaj z nowym kluczem; zobacz Klucze idempotencji w endpointach tekstowych |
| 429 | rate_limit_exceeded | Zbyt wiele żądań | Poczekaj i ponów; jeśli jest nagłówek Retry-After, podaje liczbę sekund |
| 503 | upstream_failed, model_unavailable | Model nie zadziałał lub jest przejściowo niedostępny. Nie pobrano opłaty | Spróbuj później z nowym Idempotency-Key |
Dlaczego żądanie może zwrócić 402, mimo że saldo wygląda dobrze: przed wykonaniem żądania tekstowego blokowana jest kwota pokrywająca wiadomości oraz pełne max_tokens wyniku według cen modelu (domyślne max_tokens, jeśli pominiesz pole). Płacisz tylko za rzeczywiście zużyte tokeny, a reszta blokady jest zwalniana po zakończeniu żądania, ale cała blokada musi mieścić się w dostępnym saldzie przed rozpoczęciem. Ustawienie realistycznego max_tokens zmniejsza blokadę. Zobacz Rozliczenia.
Sprawdź koszt wywołania
Każde zaakceptowane żądanie działa jako zadanie, a odpowiedź wskazuje je w nagłówku X-Spicy-Task-Id, zarówno przy strumieniowaniu, jak i bez niego. Odczytaj nagłówek, a następnie zadanie: cost jest ostateczne, gdy settled ma wartość true. Żądanie odrzucone przed akceptacją (na przykład błąd walidacji) nie tworzy zadania i nie ma tego nagłówka.
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
raw = client.chat.completions.with_raw_response.create(
model=os.environ["SPICY_MODEL"],
messages=[{"role": "user", "content": "Say hello in one sentence."}],
)
reply = raw.parse() # the usual ChatCompletion object
print(reply.choices[0].message.content)
print("task:", raw.headers.get("x-spicy-task-id"))Przy strumieniu nagłówek jest w odpowiedzi przed pierwszym fragmentem; with_raw_response w Python i .withResponse() w Node.js działają tak samo z stream: true. Pozycje w GET /api/v1/jobs zawierają też requestId wywołania, które je utworzyło, czyli wartość nagłówka odpowiedzi X-Request-Id.
Czytaj dalej
Tekst i strumieniowanie · Migracja z OpenAI lub OpenRouter · Katalog modeli i schematy · Rozliczenia · Błędy
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.
Migracja z OpenAI lub OpenRouter
Skieruj istniejący kod bibliotek OpenAI do SpicyAPI. Zmiany adresu bazowego, klucza i modelu, różnice pól żądania oraz pola usuwane przy migracji z OpenRouter.

