Приёмка LLM-providers с реальными учётными данными
ADR 0043 поставил девять providers с зелёным набором контрактных тестов — против mock. Он сам зафиксировал в «что остаётся на потом», что приёмка с реальными учётными данными для шести smoke-тестов оставалась открытой. Этот файл — место, где это «потом» отслеживается: одна строка на provider, обновляемая каждый раз, когда появляется ключ. ADR не редактируется; он — запись решения, этот файл — запись доказательства.
Золотое правило дома действует здесь целиком: capability объявляется, только когда доказана. Пока smoke не выполнен с настоящим ключом, то, что существует об этом provider, — это чтение официальной документации: хорошее, проверенное строка за строкой в Фазе 11, и всё же это не выполнение.
Состояние на 2026-09-25 — сообщение system после user/tool (AT-161)
Другая ось, та же мерка. Набор контрактных тестов теперь доказывает, куда каждый adapter
кладёт role: "system", приходящий ПОСЛЕ user или tool (в конец
messages у восьми; поднимается в верхний system у Anthropic — см.
таблицу расхождений).
ПРИНИМАЕТ ли provider такое тело — вот что спрашивает
sistema-tardio.smoke.spec.ts, и ответить может только реальный вызов:
OLLAMA_SISTEMA_TARDIO_SMOKE=1 pnpm --filter api exec vitest run test/infrastructure/llm/sistema-tardio
# providers de nuvem: exporte a mesma *_TEST_KEY dos smokes de aceite (ANTHROPIC_TEST_KEY para o Anthropic)
| provider | поздний system принят реальным provider? | как |
|---|---|---|
| Ollama | ✅ да, после user и после tool | локальный демон 0.32.15, qwen2.5-coder:7b; оба хода завершились текстом и без чанка ошибки |
| Anthropic | ❌ не доказано | нет ANTHROPIC_TEST_KEY — и в теле, которое он отправляет, позднего system в любом случае нет (поднят) |
| OpenAI | ❌ не доказано | нет OPENAI_TEST_KEY |
| OpenRouter | ❌ не доказано | нет OPENROUTER_TEST_KEY в этом окружении; в хабе ответ зависит от upstream, поэтому один прогон доказывает обслуживший его upstream |
| NVIDIA NIM | ❌ не доказано | нет NVIDIA_NIM_TEST_KEY |
| Together AI | ❌ не доказано | нет TOGETHER_TEST_KEY |
| DeepInfra | ❌ не доказано | нет DEEPINFRA_TEST_KEY |
| Bitdeer | ❌ не доказано | нет BITDEER_TEST_KEY |
| Vultr | ❌ не доказано | нет VULTR_TEST_KEY |
Smoke доказывает только ПРИЁМ (ход завершается текстом, без чанка ошибки). ПОДЧИНЯЕТСЯ ли модель указанию — вопрос качества, и он относится к AT-082.
Состояние на 2026-08-14 — capability эмбеддингов (ADR 0075)
НОВАЯ ось в этом списке. До сих пор «приёмка» означала сценарий ЧАТА;
ADR 0075 добавил
embed в контракт, и действует та же мерка: capability равна true только с доказательством
выполнения.
Ollama закрыт, и он единственный. Прогнан против настоящего локального демона (0.32.1), с моделью, загруженной на месте:
docker exec brabo-dev-ollama-1 ollama pull nomic-embed-text
OLLAMA_EMBEDDING_SMOKE=1 pnpm --filter api test ollama-provider.embeddings
| что | результат |
|---|---|
POST /api/embed, 2 входа | 2 вектора, по 768 измерений |
prompt_eval_count | 10 — estimated: false, пришло от демона |
/api/tags того же демона | nomic-embed-text с capabilities: ["embedding"] и embedding_length: 768; llama3.2:1b с ["completion","tools"] |
| модель ЧАТА, запрашивающая эмбеддинг | 501 — «This server does not support embeddings» |
501 — самая ценная находка, и она стала тестом на обоих уровнях: это уровень
МОДЕЛИ capability, падающий в самом позднем возможном месте, и причина, по которой
assertCanEmbed отказывает раньше (RN-190). Реальная
стоимость: US$ 0,00 — у локальной модели нет цены, и поэтому это единственная
приёмка на этой странице, которую можно повторять сколько угодно.
| provider | embeddings | прогнан? | причина |
|---|---|---|---|
| Ollama | ✅ да | ✅ да | локальный демон, без ключа и без затрат |
| Anthropic | ❌ нет | — | нет собственного endpoint эмбеддингов; документация указывает на третью сторону, а это другой provider с другим ключом и другим диалектом |
| OpenAI | ❌ нет | ❌ пропущен | OPENAI_TEST_KEY отсутствует |
| OpenRouter | ❌ нет | ❌ пропущен | ключ есть, но прогнанный smoke был для чата — в хабе эмбеддинг маршрутизируется к другому провайдеру, и доказательство одного endpoint не является доказательством другого |
| Together AI | ❌ нет | ❌ пропущен | TOGETHER_TEST_KEY отсутствует |
| DeepInfra | ❌ нет | ❌ пропущен | DEEPINFRA_TEST_KEY отсутствует |
| NVIDIA NIM | ❌ нет | ❌ пропущен | NVIDIA_NIM_TEST_KEY отсутствует |
| Bitdeer | ❌ нет | ❌ пропущен | BITDEER_TEST_KEY отсутствует — иронично, ведь их документация по эмбеддингам — единственный аутентифицированный источник, найденный Фазой 11b |
| Vultr | ❌ нет | ❌ пропущен | VULTR_TEST_KEY отсутствует |
То, что доказано без каких-либо учётных данных, — это ДИАЛЕКТ: набор контрактных тестов
запускается второй раз поверх OpenAI-совместимой базы с включённой capability и
проверяет упорядочивание по index, чтение usage.prompt_tokens, отказ для
неполного пакета и ошибку, нормализованную по code. Перевести provider в true,
когда ключ появится, — значит изменить одну строку литерала и прогнать smoke, а не
писать парсинг.
Состояние на 2026-08-07
Первый платный прогон в этом списке. OpenRouter закрыт; остальные пять остаются пропущенными из-за отсутствия ключа — их учётных данных нет нигде в окружении, а в продукте зарегистрирован только ключ OpenRouter (тот же, что использовался в прогоне hello-limpo).
| provider | прогнан? | результат |
|---|---|---|
| OpenRouter | ✅ да | весь сценарий зелёный, один платный вызов |
| Together AI | ❌ пропущен | TOGETHER_TEST_KEY отсутствует |
| DeepInfra | ❌ пропущен | DEEPINFRA_TEST_KEY отсутствует |
| NVIDIA NIM | ❌ пропущен | NVIDIA_NIM_TEST_KEY отсутствует |
| Bitdeer | ❌ пропущен | BITDEER_TEST_KEY отсутствует |
| Vultr | ❌ пропущен | VULTR_TEST_KEY отсутствует |
Реальная стоимость, прочитанная из token_usage тестовой базы:
| provider | модель | in | out | cost_micros | замороженная цена | estimated |
|---|---|---|---|---|---|---|
| openrouter | openai/gpt-4o-mini | 20 | 3 | 5 | 150.000 /млн | false |
Пять микро-USD — US$ 0,000005, на два порядка ниже
оценки «< US$ 0,001» из раздела выше. Здесь важно estimated: false:
потребление пришло от provider, а не от локального оценщика, а записанная цена — та,
что была заморожена в момент использования (RN-044), — а не цена
таблицы models сегодня.
Что smoke нашёл по пути
Он ни разу не запускался и поэтому молча сгнил относительно
ADR 0049, который перенёс
курирование из models.is_active в workspace_models:
- он утверждал
alvo.isActive === falseо глобальном каталоге — поле, которого больше не существует и которое приходилоundefined. Правильное утверждение RN-043 сегодня —workspaceModels.isActive(...), потому что отсутствие строки и есть выключено; - он собирал
SetModelsActiveUseCaseиSetModelBindingUseCaseсо старыми сигнатурами, безworkspaceId/curatedByи без репозитория курирования.
Ничто из этого не обнаруживалось CI: без ключа describe.skipIf пропускает
весь файл, а typecheck api выполняется по tsconfig.build.json, который
исключает test/. Приёмка, которая никогда не запускается, — не приёмка: это тот же
механизм, что оставил таблицу Фазы 10 как «не измерено», и урок действует
так же и здесь.
Исправлено вместе с прогоном: smoke теперь утверждает RN-043 путём ADR 0049. Когда остальные пять ключей появятся, оставшиеся пять smoke, вероятно, окажутся сгнившими так же и по той же причине.
Состояние на 2026-08-03
Проверка окружения на эту дату: ни одна из шести переменных не
экспортирована, и нет ключа provider под другим именем в .env. Без переменной
describe.skipIf(!apiKey) пропускает весь набор с предупреждением — это
правильное поведение, а не сбой. Ни одного платного вызова не сделано; стоимость
этого прогона — US$ 0,00.
| provider | smoke | переменная | модель по умолчанию | прогнан? | причина | реальная стоимость (token_usage) |
|---|---|---|---|---|---|---|
| OpenRouter | openrouter-provider.smoke.spec.ts | OPENROUTER_TEST_KEY | openai/gpt-4o-mini | ❌ пропущен | ключ отсутствует в окружении | — |
| Together AI | together-provider.smoke.spec.ts | TOGETHER_TEST_KEY | meta-llama/Llama-3.3-70B-Instruct-Turbo | ❌ пропущен | ключ отсутствует в окружении | — |
| DeepInfra | deepinfra-provider.smoke.spec.ts | DEEPINFRA_TEST_KEY | deepseek-ai/DeepSeek-V3 | ❌ пропущен | ключ отсутствует в окружении | — |
| NVIDIA NIM | nvidia-nim-provider.smoke.spec.ts | NVIDIA_NIM_TEST_KEY | meta/llama-3.2-3b-instruct | ❌ пропущен | ключ отсутствует в окружении | — |
| Bitdeer | bitdeer-provider.smoke.spec.ts | BITDEER_TEST_KEY | moonshotai/Kimi-K2.5 | ❌ пропущен | ключ отсутствует в окружении | — |
| Vultr | vultr-provider.smoke.spec.ts | VULTR_TEST_KEY | kimi-k2-instruct | ❌ пропущен | ключ отсутствует в окружении | — |
Файлы находятся в apps/api/test/infrastructure/llm/. Модель по умолчанию
переопределяется через <PROVIDER>_TEST_MODEL — полезно, когда у учётной записи нет доступа
к модели из колонки.
Сколько стоит закрыть каждую строку
Дёшево намеренно. Каждый smoke делает ровно один ход чата, с
prompt Responda só a palavra "ok", sem mais nada. — порядка 20 входных токенов
и горстка выходных. Всё остальное в сценарии бесплатно: проверка
учётных данных — это GET /v1/models (или GET /key) только по статусу, а синхронизация
каталога (у трёх, у которых listModels: true) — чтение.
| платные вызовы | бесплатные вызовы | порядок величины | |
|---|---|---|---|
| на smoke | 1 ход чата (~20 входных токенов) | тест соединения + sync | < US$ 0,001 |
| все шесть вместе | 6 ходов чата | — | < US$ 0,01 |
Даже на самой дорогой модели таблицы это не доходит до цента доллара за прогон. Стоит не токен, а наличие ключа.
Что доказывает каждый smoke — и это не одно и то же для всех
Разделение следует capability, ровно как спроектировал ADR 0041:
С listModels: true — OpenRouter, Together, DeepInfra. Sync обращается к
настоящему API и наполняет каталог. Именно здесь реальная цена встречает
парсер. Самый важный случай — Together: ADR 0043 фиксирует, что единица
цены (USD за миллион токенов vs. за токен) была выведена
сравнением с рыночной ценой, потому что официальная документация её не объявляет, — и
у smoke есть диапазон здравого смысла ровно для этого
(inputPricePerMillionMicros между 10.000 и 1.000.000.000; вне его
диагноз — «неверная единица», а не «дорогая модель»). Если однажды это пойдёт не так, это
находка для исправления в парсере и здесь, никогда не для замалчивания. Smoke также подтверждает
RN-043: обнаруженная модель входит выключенной, а активация — ручное
курирование.
С listModels: false — NVIDIA NIM, Bitdeer, Vultr. Ни один из трёх
не публикует цену за токен в доступной документации (NIM вообще не берёт плату за
токен: коммерческая единица — GPU/час). Поэтому sync должен пропускаться с
pulado: 'sem_capability' — и smoke утверждает ровно это, что делает
объявленную capability проверяемой, а не принятой на веру. Модель входит через seed /
ручное курирование с manual_pricing = true, и smoke реально проверяет
id модели против настоящего API — сегодня это оценка, прочитанная из документации, — и
путь metering от начала до конца с замороженной ценой.
Во всех шести шаг 1b проверяет настоящий ключ против настоящего API через
TestStoredCredentialUseCase — ok в пяти и nao_suportado в DeepInfra,
единственном без объявленного тестового endpoint. До
ADR 0050 это
доказательство происходило внутри регистрации; оно не исчезло, а сменило место и стало
явным.
Во всех шести финальный шаг одинаков, и именно он важен для продукта: полная
сессия чата, без событий error и metering_failed, со строкой в
token_usage, несущей цену, замороженную в момент использования
(RN-044), — а не сегодняшнюю цену из таблицы
models.
Как закрыть незакрытый пункт
export OPENROUTER_TEST_KEY=... # a variável do provider em questão
# opcional, se a conta não tiver o modelo default:
# export OPENROUTER_TEST_MODEL=...
pnpm --filter api test -- openrouter-provider.smoke.spec.ts
Требования:
- запущенная тестовая база —
TEST_DATABASE_URL, по умолчаниюpostgres://brabo:brabo@localhost:5432/brabo_test(та же, что у любого интеграционного теста api; smoke вызываетtruncateAllперед началом); - кредит на счёте provider. Начиная с
ADR 0050
регистрация ничего не проверяет — ключ проверяет шаг 1b smoke
(
TestStoredCredentialUseCase, только статус), а действительный ключ без баланса падает только на ходе чата.
После прогона обновите строку таблицы выше с датой, вердиктом и реальной стоимостью. Стоимость берётся из базы, а не из оценки:
SELECT provider, model_name, input_tokens, output_tokens, cost_micros
FROM token_usage ORDER BY created_at DESC LIMIT 1;
Если smoke провалится, строка становится ❌ reprovado с причиной, а находка становится
исправлением в provider — никогда capability, объявленной на доверии.
Почему это живой документ
Таблица — не отчёт на дату: это текущее состояние приёмки. Каждый
появившийся ключ закрывает строку, а остальные остаются открытыми и видимыми.
Режим отказа, ради предотвращения которого существует этот файл, — это режим Фазы 10, описанный в
урожае первого dogfooding: таблица
наблюдений, которую никто не заполнил и которая навсегда стала não medido. Здесь
число берётся из token_usage запросом, а то, что не запускалось, записано как
не запускавшееся.