spicyapiDokumentacja
Tekst i czat
Treść główna

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 above

Identyfikatory 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.

chat.py
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 zachowanieEmerytowany detektyw, który teraz opiekuje się latarnią morską
GłosJak mówi: długość zdań, słownictwo, nawyki, sposób zwracania się do użytkownikaSpokojny, głośno zauważa drobne szczegóły, mówi do użytkownika „przyjacielu”
ŚwiatMiejsce 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
GraniceCo 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
NarracjaOsoba i czas narracji oraz oznaczenie wypowiedziTrzecia osoba, czas przeszły; wypowiedzi w cudzysłowie
FormatDługość i zakończenie każdej odpowiedziNajwyż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.

roleplay.py
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:

Another card: a daily companion
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:

  1. Poproś o dane użycia, jeśli chcesz liczbę tokenów. Strumienie przestrzegają zasad OpenAI: bez stream_options.include_usage żaden fragment nie zawiera usage. Z "include_usage": true tuż przed [DONE] przychodzi dodatkowy fragment z ostatecznym usage i pustą listą choices, a każdy pozostały ma usage: null.
  2. Sprawdź choices przed odczytaniem choices[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.
  3. Rozumowanie jest osobne. Modele rozumujące strumieniują rozumowanie w delta.reasoning_content, a odpowiedź w delta.content. Pierwsze pokazuj przygaszone, zwinięte lub wcale; tylko drugie stanowi odpowiedź.
  4. finish_reason: "length" oznacza ucięcie. Model osiągnął max_tokens. Zwiększ limit lub zmniejsz reasoning_effort.
  5. 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.
  6. 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.

stream.py
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:

  1. Wyślij wiadomości razem z tools, listą funkcji opisanych za pomocą JSON Schema.
  2. 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ść tool na wywołanie z pasującym tool_call_id.
  3. 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.

tools.py
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.

json_output.py
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.

Statuserror.codeCo się stałoCo zrobić
400unsupported_parameterModel 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-ParamsUsuń je. Sprawdź pola modelu jak w Zanim zaczniesz
400invalid_requestBrakuje 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
400content_rejectedZasady treści modelu spowodowały odmowęPrzeformułuj żądanie; ponowne wysłanie tych samych danych znów spowoduje odmowę
401invalid_api_keyBrak klucza, literówka lub unieważniony kluczSprawdź SPICY_API_KEY; klucze zaczynają się od sk-spicy-
402insufficient_balance, spend_cap_exceededSaldo lub limit wydatków nie pokrywa kwoty blokowanej na to żądanieDodaj środki, podnieś limit lub obniż max_tokens (poniżej)
404model_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łaniaSkopiuj dokładny identyfikator z katalogu na żywo
404not_foundModel istnieje, ale teraz nie można go wywołaćWybierz inny model z katalogu na żywo
409conflictIdempotency-Key użyto ponownie z inną treścią żądaniaKażdą nową wiadomość wysyłaj z nowym kluczem; zobacz Klucze idempotencji w endpointach tekstowych
429rate_limit_exceededZbyt wiele żądańPoczekaj i ponów; jeśli jest nagłówek Retry-After, podaje liczbę sekund
503upstream_failed, model_unavailableModel nie zadziałał lub jest przejściowo niedostępny. Nie pobrano opłatySpró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.

task_id.py
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

Na tej stronie