SiCore TokenWorks
LLM APIAPI GatewayAggregation

Jak przełączać się między dostawcami LLM bez zmiany kodu

SiCore TokenWorks Team·2026-09-03

Większość ludzi nie zaczyna projektu od myślenia o przenośności między dostawcami. Biorą klucz od tego dostawcy, u którego rejestracja jest najłatwiejsza, piszą integrację i wypuszczają produkt. Kilka miesięcy później czytają benchmark, albo otwierają rachunek, albo wpadają na limit zapytań i chcą wypróbować coś innego. Wtedy pojawia się ból, bo w warstwę HTTP wbudowali założenia dotyczące pierwszego dostawcy.

Rozwiązaniem nie jest jakaś biblioteka abstrakcji. To format transmisji. API OpenAI chat completions po cichu stało się domyślnym protokołem komunikacji z LLM i gdy raz napiszesz kod pod nie, przełączanie dostawców to głównie kwestia zmiany dwóch ciągów znaków.

API, które skopiowali wszyscy

OpenAI zdefiniowało prosty kontrakt: wysyłasz POST z JSON-em na /v1/chat/completions, a w odpowiedzi dostajesz ciąg znaków choices[0].message.content. Żądanie wygląda tak:

curl https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-4o",
    "messages": [{"role": "user", "content": "Explain DNS in one paragraph"}]
  }'

Niemal każdy duży model mówi teraz tym dialektem. DeepSeek, Qwen, ERNIE, Doubao i pozostałe udostępniają endpoint, który przyjmuje to samo ciało żądania i zwraca ten sam kształt odpowiedzi. Oznacza to, że twój kod klienta nie dba o to, kto jest po drugiej stronie. Możesz podmienić ciąg znaków model i endpoint, a nic więcej się nie zmienia.

Oto to samo wywołanie skierowane do agregatora, który obsługuje kilka z tych modeli pod jednym bazowym URL-em:

curl https://api.token8341.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKENWORKS_API_KEY" \
  -d '{
    "model": "deepseek-chat",
    "messages": [{"role": "user", "content": "Explain DNS in one paragraph"}]
  }'

Jeden klient, wiele modeli

W Pythonie zwykle tworzysz instancję SDK OpenAI raz i przekazujesz nazwę modelu przy każdym żądaniu. Jeśli twój dostawca obsługuje kompatybilne API, ustawiasz base_url raz i wszystko inne pozostaje identyczne:

from openai import OpenAI

client = OpenAI(
    base_url="https://api.token8341.com/v1",
    api_key="sk-your-tokenworks-key",
)

def ask(model: str, prompt: str) -> str:
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
    )
    return resp.choices[0].message.content

# Same function, three different model families.
print(ask("gpt-4o", "Summarize this log for me."))
print(ask("deepseek-chat", "Summarize this log for me."))
print(ask("qwen-max", "Summarize this log for me."))

Funkcja się nie zmienia. Zmienia się ciąg znaków modelu. Na tym polega cała sztuczka. Strumieniowanie działa tak samo:

stream = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "Write a haiku about databases"}],
    stream=True,
)
for chunk in stream:
    delta = chunk.choices[0].delta.content or ""
    print(delta, end="")

Wywoływanie funkcji, tryb JSON i embeddingi działają na tej samej kompatybilnej powierzchni, więc przy przełączaniu nie jesteś ograniczony do zwykłego czatu.

Czego przełączenie ci nie daje

Chcę być jasny co do części, która nie zmienia się za darmo. Transport jest przenośny; modele nie są.

Różne modele reagują na prompty w różny sposób. System prompt dostrojony pod GPT-4o może działać słabiej na modelu rozumującym, który oczekuje innej struktury. Okna kontekstu się różnią, czasem znacząco: jeden model może przyjąć 128k tokenów, a inny 32k. Tokenizatory też się różnią, więc ten sam dokument kosztuje inną liczbę tokenów na każdym modelu. Maksymalna długość wyjścia to kolejny parametr, który jest właściwy dla modelu, a nie dla protokołu.

Więc „przełączanie bez zmiany kodu" to tak naprawdę „przełączanie bez przepisywania klienta HTTP". Wciąż należy ci się przebieg ewaluacji, zanim skierujesz ruch produkcyjny na nowy model. Dobra wiadomość jest taka, że kompatybilne API sprawia, iż ta ewaluacja jest tania w uruchomieniu, bo harness to po prostu pętla po nazwach modeli.

Praktyczny wzorzec

To, co faktycznie robię w projektach pobocznych, to trzymanie małej tabeli konfiguracji, a nie zmian w kodzie:

MODELS = {
    "fast": "deepseek-chat",
    "smart": "qwen-max",
    "frontier": "gpt-4o",
}

def run(task, prompt):
    return ask(MODELS[task], prompt)

Chcesz wypróbować tańszy model dla ścieżki fast? Zmieniasz jedną linię. Chcesz przełączyć się awaryjnie na drugiego dostawcę, gdy pierwszy ma limit zapytań? Przechwytujesz wyjątek i ponawiasz z następnym ciągiem znaków modelu. Nic z tego nie dotyka konstrukcji żądania.

Ten wzorzec awaryjny to miejsce, w którym wielomodelowy endpoint zarabia na siebie. Z jednym bazowym URL-em i jednym kluczem możesz routować, ponawiać i przeprowadzać testy A/B między dostawcami, wszystko przez te same dwie linie konfiguracji klienta, które napisałeś pierwszego dnia.

Osobliwości odpowiedzi, na które warto uważać

Transport jest kompatybilny, ale kilka różnic na poziomie odpowiedzi wciąż się przebija. Modele rozumujące czasem zwracają dodatkowe pole, takie jak reasoning_content, obok normalnej wiadomości. Kod, który czyta choices[0].message.content, nadal działa, ale możesz chcieć pokazać ten tekst rozumowania w widoku debugowania. Komunikaty błędów też się różnią: jeden dostawca zwraca pomocny ciąg insufficient_quota, inny zwraca gołe 429 bez ciała. Jeśli budujesz ponawianie, traktuj każde 429 lub 5xx jako „odczekaj i spróbuj ponownie", zamiast parsować tekst specyficzny dla dostawcy.

Nic z tego nie jest powodem, by pozostawać przy jednym dostawcy. To powód, by utrzymywać obsługę błędów w ogólnej formie, a wybór modelu jako zmienną.

Takie stanowisko zajmuje SiCore TokenWorks: kompatybilny z OpenAI endpoint pod https://api.token8341.com/v1, gdzie ciąg znaków modelu to jedyna rzecz, która się zmienia, gdy przechodzisz między GPT-4o, Claude, Gemini, DeepSeek, Qwen, ERNIE, Doubao, Spark i Pangu.