Прямые вызовы модели
Пул (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=, которого нет в платном каталоге, бросает эту ошибку на старте и перечисляет доступные.MissingKeyError—api_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__.