0.167.25

Fixed

  • max_completion_tokens отправлялся всем бэкендам безусловно, а extra_body молча выбрасывался (#299 — P3). (1) buildParams всегда ставил params.MaxCompletionTokens — бэкенды, принимающие только классический max_tokens (часть сборок vLLM, LM Studio, отдельные шлюзы), отвергали запрос как unknown parameter либо молча игнорировали лимит, оставляя генерацию неограниченной. Теперь wire-поле выбирается по ProviderSpec.SupportsMaxCompletionTokens (реестр: true только у OpenAI; остальные получают max_tokens, который принимают все OpenAI-совместимые), а неположительный лимит (maxTokens <= 0) не отправляется вовсе. (2) extraBody из конфигурации (models.extra_body / llm_providers.extra_body) парсился и отбрасывался параметром-заглушкой _ конструктора — настройка выглядела работающей. Теперь карта сохраняется в провайдере (с defensive-копией: buildParams читает её без локов) и мерджится в тело запроса через SetExtraFields. Правило приоритета: типизированные поля запроса выигрывают — конфликтующие ключи extra_body отбрасываются с WARN (SetExtraFields иначе молча переопределил бы model/messages/tools); в защищённый набор входят и поля, устанавливаемые самим SDK (stream, stream_options), и намеренно опускаемые (temperature у reasoning-моделей — опущение само по себе типизированное решение). Незанятые ключи (top_k, repetition_penalty, safe_prompt, provider-роутинг OpenRouter…) проходят насквозь дословно. Тесты: флаг true/false → max_completion_tokens/max_tokens в запросе, maxTokens<=0 → поле отсутствует; extra_body мерджится дословно; model/temperature/max_tokens/reasoning_effort из extra_body не затирают типизированные; temperature не «воскресает» на reasoning-модели; nil/пустая карта → no-op.