Документация API go2llm

Три модели вместо одной: дешёвая на рутину, дорогая на архитектуру

Технический разбор от команды go2llm, не отзыв клиента. Код рабочий, арифметика показана целиком — пересчитайте сами. Все суммы — расчёт по опубликованному прайсу (каталог v38, 2026-09-27), а не замер чужого счёта.

Задача

В приложении один LLM-вызов обслуживает всё: нормализацию текста, извлечение полей, ответ пользователю и разбор архитектурного вопроса. Модель выбрана по худшему случаю — самая сильная, потому что «иначе на сложных запросах плохо». Результат: 90% трафика решается моделью в разы дешевле, но платите вы по верхней ставке за всё.

Нужно разложить трафик по трём уровням, оставив один ключ, один баланс и одну кодовую базу. И нужна честная арифметика: роутер сам стоит денег, и надо знать, когда он окупается.

Как было

# ДО: одна модель на всё
import os

from openai import OpenAI

client = OpenAI(
    api_key=os.environ["GO2LLM_API_KEY"],
    base_url="https://go2llm.tech/v1",
)

def run(system: str, user: str, max_tokens: int = 1200) -> str:
    resp = client.chat.completions.create(
        model="claude-opus-5",          # выбрана по худшему случаю
        max_tokens=max_tokens,
        messages=[
            {"role": "system", "content": system},
            {"role": "user", "content": user},
        ],
    )
    return resp.choices[0].message.content

Проблема не в коде, а в экономике: claude-opus-5 стоит 0.503 / 2.51 $ за 1M токенов вход/выход, claude-haiku-4-5 — 0.13 / 0.65, то есть примерно в 3.9 раза дешевле на обеих сторонах.

Что делаем

1. Определяем уровни, а не модели

Уровень задаётся тем, что требуется от ответа, и описывается словами до того, как выбирается модель:

УровеньЧто этоПример
cheapдетерминированное преобразование, короткий ответ, есть эталонизвлечь поля, нормализовать, классифицировать
mainобычная содержательная работа, нужен связный текст и следование инструкцииответ пользователю, ревью диффа, черновик
hardмногошаговое рассуждение, архитектура, компромиссы, длинный контекстразбор трейдоффов, план миграции

2. Сначала правила, только потом LLM-классификатор

Дешёвый классификатор всё равно стоит денег и добавляет задержку. Большая часть трафика классифицируется бесплатно — по типу вызова в вашем коде.

3. Фиксируем model ID из каталога

curl -sS https://go2llm.tech/v1/models \
  | jq -r '.data[] | select(.id | test("haiku|sonnet|opus|deepseek")) | .id'

Строки копируются из вывода: приблизительное написание даёт 404 model_not_found.

4. Ограничиваем ключ ровно теми моделями, что есть в роутере

На каждый ключ настраивается список разрешённых моделей, квота в долларах, IP allow-list, срок и лимит 20 RPM по умолчанию. Если в списке только маршруты роутера, ошибка в коде вернёт 403 model, а не счёт за лишнюю модель.

5. Пишем роутер

# router.py
import json
import os
import re
from dataclasses import dataclass
from enum import Enum

import openai
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["GO2LLM_API_KEY"],
    base_url="https://go2llm.tech/v1",
    max_retries=0,
    timeout=90.0,
)


class Tier(str, Enum):
    CHEAP = "cheap"
    MAIN = "main"
    HARD = "hard"


ROUTES: dict[Tier, dict] = {
    Tier.CHEAP: {"model": "claude-haiku-4-5", "max_tokens": 500},
    Tier.MAIN:  {"model": "claude-sonnet-5",  "max_tokens": 900},
    Tier.HARD:  {"model": "claude-opus-5",    "max_tokens": 2000},
}

CLASSIFIER_MODEL = "deepseek-v4-flash"   # 0.0226 / 0.0679 $ за 1M

# Бесплатный слой: то, что можно решить без вызова LLM.
_HARD_MARKERS = re.compile(
    r"\b(архитектур|трейдофф|trade-?off|миграц|отказоустойчив|"
    r"почему именно|сравни подход|спроектируй)\w*", re.I
)
_CHEAP_MARKERS = re.compile(
    r"\b(извлеки|нормализуй|переведи|классифицируй|отформатируй|"
    r"переформулируй|проверь орфограф)\w*", re.I
)

CLASSIFIER_PROMPT = (
    "Ты классификатор сложности запроса. Ответь строго JSON вида "
    '{"tier":"cheap|main|hard"}. '
    "cheap — механическое преобразование текста или извлечение полей. "
    "main — обычный содержательный ответ. "
    "hard — многошаговое рассуждение, архитектура, сравнение компромиссов."
)


@dataclass(slots=True)
class Result:
    text: str
    tier: Tier
    model: str
    cost_usd: float
    classifier_cost_usd: float

    @property
    def total_cost_usd(self) -> float:
        return self.cost_usd + self.classifier_cost_usd


def _cost(resp) -> float:
    return float(getattr(resp.usage, "cost_usd", 0.0) or 0.0)


def classify(user: str) -> tuple[Tier, float]:
    """Возвращает (уровень, стоимость классификации в USD)."""
    head = user[:1500]

    # Слой 0: правила. Стоит ноль.
    if _CHEAP_MARKERS.search(head) and len(head) < 600:
        return Tier.CHEAP, 0.0
    if _HARD_MARKERS.search(head) or len(user) > 6000:
        return Tier.HARD, 0.0

    # Слой 1: дешёвая модель. Короткий вход, 20 токенов на выход.
    try:
        resp = client.chat.completions.create(
            model=CLASSIFIER_MODEL,
            max_tokens=20,
            temperature=0,
            response_format={"type": "json_object"},
            messages=[
                {"role": "system", "content": CLASSIFIER_PROMPT},
                {"role": "user", "content": head},
            ],
        )
        raw = json.loads(resp.choices[0].message.content)
        return Tier(raw.get("tier", "main")), _cost(resp)
    except (openai.APIStatusError, ValueError, json.JSONDecodeError):
        # Классификатор упал или вернул мусор — деградируем в середину,
        # а не вверх: неправильный hard дороже неправильного main.
        return Tier.MAIN, 0.0


def run(system: str, user: str, force: Tier | None = None) -> Result:
    tier, cls_cost = (force, 0.0) if force else classify(user)
    route = ROUTES[tier]
    resp = client.chat.completions.create(
        model=route["model"],
        max_tokens=route["max_tokens"],
        messages=[
            {"role": "system", "content": system},
            {"role": "user", "content": user},
        ],
    )
    return Result(
        text=resp.choices[0].message.content,
        tier=tier,
        model=route["model"],
        cost_usd=_cost(resp),
        classifier_cost_usd=cls_cost,
    )

Как стало

Вызывающий код не знает про модели, он знает про уровни — и может форсировать уровень там, где сложность известна заранее.

# app.py
from router import Tier, run

# Тип вызова известен — классификатор не нужен, force экономит его стоимость.
fields = run(
    "Извлеки поля в JSON: сумма, дата, контрагент.",
    invoice_text,
    force=Tier.CHEAP,
)

# Тип вызова неизвестен — пусть решает классификатор.
answer = run("Ты инженерный ассистент.", user_question)

print(answer.tier, answer.model, f"${answer.total_cost_usd:.6f}")

Учёт расхода по уровням обязателен: без него вы не узнаете, что классификатор сваливает 40% трафика в hard:

# metrics.py
from collections import defaultdict

spend = defaultdict(float)
calls = defaultdict(int)


def track(result) -> None:
    spend[result.tier] += result.total_cost_usd
    calls[result.tier] += 1


def report() -> str:
    total = sum(spend.values())
    lines = [f"total ${total:.4f}"]
    for tier in spend:
        share = spend[tier] / total * 100 if total else 0.0
        avg = spend[tier] / calls[tier]
        lines.append(
            f"{tier.value:<6} n={calls[tier]:<6} "
            f"${spend[tier]:.4f} ({share:.1f}%) avg=${avg:.6f}"
        )
    return "\n".join(lines)

Смоук по всем маршрутам перед выкаткой:

for m in deepseek-v4-flash claude-haiku-4-5 claude-sonnet-5 claude-opus-5; do
  printf '%-20s ' "$m"
  curl -sS https://go2llm.tech/v1/chat/completions \
    -H "Authorization: Bearer $GO2LLM_API_KEY" \
    -H "content-type: application/json" \
    -d "{\"model\":\"$m\",\"max_tokens\":40,
         \"messages\":[{\"role\":\"user\",\"content\":\"Ответь одним словом: работает\"}]}" \
    | jq -r '[.choices[0].message.content, (.usage.cost_usd|tostring)] | @tsv'
done

Сколько это стоит

Расчёт по опубликованному прайсу, каталог v38 от 2026-09-27, а не замер у клиента. Проверить: /pricing и GET /api/pricing.

Цены, $ за 1M токенов:

Модельinout
deepseek-v4-flash0.02260.0679
claude-haiku-4-50.130.65
claude-sonnet-50.3291.64
claude-opus-50.5032.51

Смешанный профиль, 100 000 запросов в месяц:

  • классификация на каждом запросе: 500 in / 20 out, deepseek-v4-flash;
  • 55 000 запросов (55%) — cheap, 1 500 in / 500 out на claude-haiku-4-5;
  • 35 000 запросов (35%) — main, 3 000 in / 900 out на claude-sonnet-5;
  • 10 000 запросов (10%) — hard, 8 000 in / 2 000 out на claude-opus-5.

Арифметика по строкам

Классификация, deepseek-v4-flash: 100 000 × 500 = 50M вход → 50 × 0.0226 = $1.13 100 000 × 20 = 2M выход → 2 × 0.0679 = $0.1358 итого $1.2658

Уровень cheap, claude-haiku-4-5: 55 000 × 1 500 = 82.5M вход → 82.5 × 0.13 = $10.725 55 000 × 500 = 27.5M выход → 27.5 × 0.65 = $17.875 итого $28.60

Уровень main, claude-sonnet-5: 35 000 × 3 000 = 105M вход → 105 × 0.329 = $34.545 35 000 × 900 = 31.5M выход → 31.5 × 1.64 = $51.66 итого $86.205

Уровень hard, claude-opus-5: 10 000 × 8 000 = 80M вход → 80 × 0.503 = $40.24 10 000 × 2 000 = 20M выход → 20 × 2.51 = $50.20 итого $90.44

Итого с роутингом: 1.2658 + 28.60 + 86.205 + 90.44 = $206.51

Сравнение с одной моделью на всё

Те же объёмы токенов, без классификатора, всё на claude-opus-5:

  • cheap-объём: 82.5 × 0.503 = 41.4975 + 27.5 × 2.51 = 69.025 = $110.52
  • main-объём: 105 × 0.503 = 52.815 + 31.5 × 2.51 = 79.065 = $131.88
  • hard-объём: $90.44
  • итого $332.84

Экономия: 332.84 − 206.51 = $126.33, то есть 126.33 / 332.84 = 38.0%, или в 332.84 / 206.51 = 1.61 раза дешевле.

Справочный рублёвый эквивалент по курсу ЦБ 86,3793 ₽ за $1 на 01.09.2026: $332.84 ≈ 28 751 ₽ против $206.51 ≈ 17 838 ₽, разница ≈ 10 913 ₽ в месяц. Баланс и списания ведутся в USD.

Честная контрольная точка: а если всё на Sonnet?

Тот же объём целиком на claude-sonnet-5:

  • cheap-объём: 82.5 × 0.329 = 27.1425 + 27.5 × 1.64 = 45.10 = $72.24
  • main-объём: $86.205
  • hard-объём: 80 × 0.329 = 26.32 + 20 × 1.64 = 32.80 = $59.12
  • итого $217.57

То есть роутинг стоит на 217.57 − 206.51 = $11.06 дешевле (−5.1%), чем «всё на Sonnet» — в этом каталоге Opus относительно официальной цены дешевле, чем Sonnet (скидка около 90% против 84%), и доплата за 10% трафика наверх, на Opus, не перекрывает экономию от увода 55% вниз, на Haiku. При другом наборе моделей или других пропорциях трафика соотношение может стать обратным — это не универсальный вывод, а точная картина при этих цифрах. Ценность роутинга не в ±5%, а в том, что вы платите за качество ровно там, где оно нужно, вместо равномерной ставки по всему трафику.

Нижняя граница для сравнения — всё на claude-haiku-4-5: 28.60 + (105 × 0.13 + 31.5 × 0.65) + (80 × 0.13 + 20 × 0.65) = 28.60 + 34.125 + 23.40 = $86.13. Дешевле в 2,5 раза, чем «всё на Sonnet», но hard-запросы обслуживаются моделью, выбранной не под них.

Сколько стоит сам роутер

Классификация — $1.2658 из $206.51, то есть 0.6% счёта. Тот же классификатор на claude-haiku-4-5 стоил бы 50 × 0.13 + 2 × 0.65 = 6.50 + 1.30 = $7.80, в 6,2 раза дороже. На glm-5.3-flash (0.018 / 0.06): 50 × 0.018 + 2 × 0.06 = $1.02.

Правило окупаемости: стоимость классификации должна быть заметно меньше разницы между уровнями. Если cheap и main различаются на копейки, классификатор — чистый убыток плюс лишняя задержка, и работают только бесплатные правила слоя 0.

Грабли

1. Классификатор, деградирующий вверх. Если при сбое парсинга JSON роутер падает в hard, стоимость ошибки максимальна. В коде выше fallback идёт в MAIN намеренно: неправильный main дешевле неправильного hard.

2. 404 model_not_found на одном из трёх маршрутов. Опечатка в редком маршруте вскроется не сразу, а на первом hard-запросе в проде. Проверяйте все маршруты на старте через GET /v1/models.

3. 403 model после сужения списка моделей на ключе. Добавляя четвёртый маршрут в код, не забудьте добавить модель в ключ. Ошибка выглядит как отказ авторизации, хотя ключ действующий.

4. 429 rate_limit из-за удвоения запросов. Роутер с LLM-классификатором делает два запроса вместо одного. При лимите 20 RPM по умолчанию эффективная пропускная способность падает вдвое. Слой правил, решающий часть случаев бесплатно, снимает и эту проблему, а не только денежную.

5. 402 insufficient_balance в середине пайплайна. Классификатор прошёл, основной вызов упал: пользователь остался без ответа, а деньги за классификацию потрачены. Следите за балансом заранее.

6. 502 upstream_unavailable только на одном уровне. Доступность зависит от внешних поставщиков. До начала ответа шлюз может использовать совместимый fallback-маршрут на разрешённых классах ошибок, но после первого байта или валидного SSE-события переключение не выполняется. Разумный прикладной fallback — деградировать hard в main при устойчивом 502, а не ретраить тот же маршрут четвёртый раз. Коды — /docs/errors.

Лимит 20 RPM считается на ключ, а не на модель. Он суммарный по всем моделям и по всем endpoints сразу, поэтому вызов классификатора и основной вызов расходуют один и тот же счётчик — роутер с LLM-классификатором действительно удваивает нагрузку на лимит. Слой правил, отвечающий на часть запросов без обращения к модели, снимает и это. Отдельно действует лимит параллельных запросов на аккаунт — по умолчанию 2.

Prompt caching работает на обоих протоколах, включая /v1/chat/completions, и тарифицируется: запись кэша идёт по цене in, чтение — по цене cache из прайса. Важная деталь: шлюз кэш не включает сам — он работает от того, что клиент положил в тело запроса. В расчёте выше кэш намеренно не учитывался, поэтому реальный счёт на повторяющихся системных промптах будет ниже приведённого.

Поддержку response_format: {"type":"json_object"} и structured outputs мы пока не документировали — напишите в @go2llm_support, ответим и дополним страницу. До тех пор закладывайте в роутер устойчивый ручной разбор ответа классификатора: требуйте одно слово из фиксированного списка и падайте в main на любом неожиданном выводе.

Итог

Роутинг по сложности — не «включить дешёвую модель», а решение о том, где именно вы готовы платить за качество. На разобранном профиле переход с «всё на Opus 5» на три уровня даёт $332.84 → $206.51, минус 38.0% при 100 000 запросов в месяц. Сам роутер стоит 0.6% счёта.

Технически это возможно потому, что один ключ gk_sk_… покрывает все опубликованные модели, а endpoint один и тот же: смена уровня — это смена строки model, а не второй провайдер и второй биллинг. Перед выкаткой прогоните смоук по всем маршрутам и ограничьте ключ списком моделей из ROUTES.

Документация — /docs, цены — /pricing, коды ошибок — /docs/errors, главная — go2llm.tech.

Получить ключ и сделать первый запрос за 3 минуты