Skip to content

Прямые вызовы модели

Пул (ask/chat/stream) роутит по многим моделям с фейловером. Иногда нужна одна конкретная модель, вызываемая напрямую — платная frontier-модель для задачи на качество. Это и даёт broker.direct(...): клиент ровно к этой модели, без пула и без фейловера.

Прямой доступ — для моделей, объявленных через direct= там, где вы создаёте брокер. Пуловые модели анонимны: к ним обращаются через ask/chat/stream, которые роутят и учатся; попытка назвать пуловую модель бросает PoolModelError.

Объявить и вызвать

broker = llmbroker.Broker(direct=["opus"])
broker.direct("opus").ask("...")

"opus"алиас из курируемого каталога платных провайдеров, вечная ручка. Выйдет следующее поколение Claude — llmbroker перенаправит opus на него, а ваш код не изменится. Алиас никогда не исчезает, не переименовывается и не содержит номера версии. Задайте нужный ему ключ: алиас сам приводит к ANTHROPIC_API_KEY, OPENAI_API_KEY и так далее — по провайдеру.

Для объявленной модели никуда ничего не пишется. Список в вашем коде — единственный источник правды, а алиас разрешается заново по тем же суточным часам, по которым обновляется пул: именно это и держит его на текущей версии, без синхронизации и без файла, который надо обновлять.

Если в момент срабатывания этих часов каталог недоступен, модель остаётся на той версии, которую уже обслуживает, а в лог уходит предупреждение. Работающее разрешение никогда не меняется на более старое; упасть может только самое первое, на старте, — там же опечатка в алиасе и скажет о себе, перечислив существующие.

Модель целиком ваша

Передайте вместо алиаса конфиг — self-hosted endpoint, корпоративный шлюз или версия, которая двигаться не должна:

from llmbroker import LLMConfig

gateway = LLMConfig(
    name="frontier",
    model="claude-opus-4-8",
    base_url="https://api.anthropic.com/v1",   # любой OpenAI-совместимый endpoint
    api_key_ref="ANTHROPIC_API_KEY",
)
broker = llmbroker.Broker(direct=[gateway])
broker.direct(name="frontier").ask("...")

Такая модель ваша вплоть до версии: никакое обновление её не тронет, потому что llmbroker не сказали, за какой строкой каталога она следует.

Пул не примет модель, объявленную здесь

Не «по умолчанию» — никогда. Объявленная модель не роутится, на неё не уходит фейловер, её нет в count() и snapshot(). Вся ценность пула — фейловер между взаимозаменяемыми бесплатными endpoint'ами, курируемыми как один набор; частный шлюз, брошенный туда, получал бы чужой rate limit и трафик, предназначенный бесплатному ярусу.

Свой endpoint может стать участником пула — если записать его в свой реестр, где обновление его не тронет. Это решение принимается один раз и записывается там, а не получается побочным эффектом от того, что модель захотели вызвать по имени. В реестре лежат только участники пула, так что положить модель туда — это выбор, противоположный объявлению её здесь.

Как найти платную модель

llmbroker list печатает оба курируемых списка и ничего не пишет. Строка direct даёт алиас, который надо объявить, а дальше — id провайдера, id модели, base_url и api_key_ref, которые закреплённое объявление задаёт само:

$ llmbroker list
pool groq-gpt-oss-120b openai/gpt-oss-120b https://api.groq.com/openai/v1 GROQ_API_KEY
...
direct opus anthropic claude-opus-5 https://api.anthropic.com/v1 ANTHROPIC_API_KEY
direct sonnet anthropic claude-sonnet-5 https://api.anthropic.com/v1 ANTHROPIC_API_KEY

Чтение каталога из программы

llmbroker list — для человека. Те же два курируемых файла читаются и как данные: без брокера и без сети — берётся копия, уже лежащая на этой машине, а под ней копия из колеса:

from llmbroker import curated_paid, curated_pool, curated_providers

for row in curated_paid():
    print(row.alias or "-", row.name, row.label)

for provider in curated_providers():
    print(provider.id, provider.base_url, provider.api_key_ref)

print(len(curated_pool().configs), "бесплатных моделей в курируемом списке")

Отсюда же берётся модель, которой в каталоге нет — свежий релиз или та, которую хочется замерить до того, как её кто-то закурирует. Попросите у провайдера объявление и передайте его в direct=:

anthropic = next(p for p in curated_providers() if p.id == "anthropic")
broker = llmbroker.Broker(direct=[anthropic.declare("claude-opus-9-preview")])
broker.direct(name="anthropic-claude-opus-9-preview").ask("...")

declare() подставит base url и ссылку на ключ; всё остальное — тот же полностью описанный конфиг, что и в разделе выше, поэтому его никто и никогда не переназначит. У курируемой строки тоже есть .declare(), и она фиксирует именно её id модели — передайте строку-алиас, если хотите, чтобы модель продолжала следовать за каталогом.

Ничто здесь ничего не обновляет: эти функции читают, пишет sync. Что бросается при отсутствующем ключе — в разделе Ошибки.

alias и name — раздельные пространства имён

Место вызова само говорит, что имеется в виду. Это делает direct(name=...) ещё и проверкой версии: укажите anthropic-claude-opus-5, и в день, когда каталог переведёт алиас дальше, вызов упадёт с ошибкой, а не тихо уйдёт на новую модель.

Ничего из объявленного никуда не пишется — ни значения ключа (никогда), ни даже конфига. Ключ читается из переменной окружения или secrets-бэкенда в момент вызова.

Следовать за каталогом, не теряя свою версию

Объявленный алиас разрешается заново по тем же суточным часам, по которым обновляется пул. Когда каталог его переводит, в лог уходит одна строка с обеими версиями:

direct=: opus: claude-opus-4-8 -> claude-opus-5

После перевода у модели новое name — оно несёт версию. Если модель переехала к другому провайдеру, в строке будет и новый api_key_ref — задайте эту переменную окружения до следующего вызова.

Объявление, выписанное целиком, не переводится никогда: llmbroker не сказали, за какой строкой каталога оно следует.

Стриминг и ask (async)

async with llmbroker.AsyncBroker(direct=["opus"]) as broker:
    client = await broker.direct("opus")

    # стриминг — async-итератор текстовых дельт
    async for delta in client.stream("Напиши хокку про брокеров"):
        print(delta, end="", flush=True)

    # либо весь ответ сразу
    result = await client.ask("Дай полный текст")
    print(result.text, result.usage)

Здесь стрим идёт к одной названной модели: ни роутинга, ни фейловера, ни записи в журнал. Стриминг из пула — в Асинхронности.

Синхронно

У блокирующего Broker тоже есть direct(...), только с ask() (стриминг — async-only):

with llmbroker.Broker(direct=["opus"]) as broker:
    result = broker.direct("opus").ask("...")
    print(result.text)

Параметры запроса

Модель названа вами, значит ей можно отправить всё, что она документирует, — бюджет рассуждения, температуру, лимит токенов, seed. params — это словарь, который дословно подмешивается в тело запроса: оба клиента принимают его в ask(), а асинхронный клиент — ещё и в stream():

client = broker.direct("opus")
client.ask("...", params={"reasoning_effort": "low", "temperature": 0})

llmbroker не читает, не проверяет, не переписывает и не подставляет ничего из params и не обещает, что провайдер это учтёт: неподдерживаемый параметр — ваша ошибка, и сообщение самого провайдера и есть отчёт о ней. Внутри params — словарь провайдера, рядом с ним (messages=, timeout=) — словарь llmbroker.

Пять ключей, которые llmbroker строит сам, — model, messages, stream, stream_options, tools — вместо подмешивания бросают ValueError с именем ключа: подменённый model ответил бы моделью, которую никто не называл, а переключённый флаг стриминга отдал бы тело не тому читателю. tool_choice в этот список не входит, поэтому params={"tool_choice": "required"} перекрывает "auto", выставленный вместе с tools.

Это только прямой путь. Пул несёт параметр лишь там, где он взвешен для всего пула по одному — см. вывод по схеме.

Ошибки

Прямые вызовы бросают из одной иерархии под LLMRequestError:

  • PoolModelError — названа managed-модель пула. Используйте ask/chat/stream либо объявите модель сами.
  • UnknownModelError — подходящей записи нет. Если строка есть в другом пространстве имён, сообщение об этом скажет. Алиас в direct=, которого нет в платном каталоге, бросает эту ошибку на старте и перечисляет доступные.
  • MissingKeyErrorapi_key_ref модели не задан (платная модель без ключа — здесь это ошибка, в отличие от пуловой, которая просто остаётся неактивной).
  • ProviderError — провайдер вернул ошибку, с .status и .detail. Ловите грубо либо наследников AuthError (401/403) и RateLimitError (429/503, с .retry_after) для точечной обработки.
  • InvalidProviderResponseError — HTTP 200 с телом, которое не является chat completion (не разбирается или в нём нет ответа ассистента), либо является им, но не несёт ответа вовсе — ни текста, ни вызовов инструментов; с .model и фрагментом в .detail. Прятать это за failover здесь нечем: названная вами модель ответила мусором или ничем.
  • LLMTimeoutError — вызов не уложился в таймаут.
  • StreamInterruptedErrorпуловый стрим оборвался после того, как дельты уже пошли; в .llm_name — модель, причина приложена как __cause__.