feat: нативное поле рассуждений (reasoning_field) — правка 400 от Console Go
Build LLM Proxy / Build and push (release) Successful in 39s
Build LLM Proxy / Build and push (release) Successful in 39s
Диагноз: апстрим deepseek-v4.1-flash (провайдер opencode, Console Go) в thinking-режиме требует reasoning_content в assistant-сообщениях с tool_calls, а клиент opencode присылает рассуждения как reasoning + reasoning_details. Отсюда 400 'The reasoning_content in the thinking mode must be passed back to the API'. - providers[].reasoning_field / reasoning_empty_ok: прокси аддитивно достраивает нативное поле в assistant-сообщениях с непустым tool_calls (текст из reasoning или reasoning_details[].text, тип reasoning.text); существующее непустое поле не перезаписывается, ничего не переименовывается, прочие сообщения не трогаются; - устойчивость разбора ответов к JSON-null (choices/delta/content/tool_calls) — было 290 фейловеров с локального апстрима на платные из-за нашего же исключения; - лог: класс исключения в сообщении об ошибке; для 4xx логируется тело ответа апстрима (читается безопасно: 4xx — не стрим); - тесты: ReasoningFieldTest (13), NullToleranceTest (6), ConfigLogicTest (+2) — 100 всего; - CONFIG.md, TESTING.md (фактические замеры A/B против Console Go).
This commit is contained in:
@@ -218,6 +218,44 @@ upstreams:
|
||||
рассуждения». Если `content` не строка (мультимодальный массив частей) — ответ
|
||||
не трогаем. Без флажка (`off`) ответ идёт байт-в-байт как раньше.
|
||||
|
||||
### Нативное поле рассуждений (`reasoning_field`)
|
||||
|
||||
Некоторые шлюзы-апстримы (например, **Console Go / deepseek в thinking-режиме**)
|
||||
в thinking-режиме **требуют** вернуть им нативное поле рассуждений в каждом
|
||||
assistant-сообщении с непустым `tool_calls`. Если его нет — апстрим отвечает
|
||||
`400` (`The `reasoning_content` in the thinking mode must be passed back to the
|
||||
API.`). Клиенты при этом рассуждения держат в своих форматах: `reasoning`
|
||||
(строка) и/или `reasoning_details` (массив `{type:"reasoning.text", text, ...}`)
|
||||
— а нативное поле `reasoning_content` в истории могут и не передавать.
|
||||
|
||||
Поля (только на уровне **провайдера**, это свойство шлюза, а не модели):
|
||||
|
||||
| Поле | Тип / дефолт | Значение |
|
||||
|---|---|---|
|
||||
| `providers[].reasoning_field` | строка / отсутствует | Имя нативного поля рассуждений у шлюза-апстрима (пример: `reasoning_content`) |
|
||||
| `providers[].reasoning_empty_ok` | булево / `false` | Писать ли пустую строку, если текста рассуждений нет вовсе |
|
||||
|
||||
Зачем: при отправке запроса прокси **аддитивно** достраивает это поле в каждом
|
||||
assistant-сообщении с непустым `tool_calls` — берёт текст из `reasoning`
|
||||
(если это непустая строка), иначе склеивает `reasoning_details[*].text`
|
||||
(только элементы без `type` или с `type == "reasoning.text"`, через `"\n"`) и
|
||||
записывает в поле `reasoning_field`, **если его там ещё нет**. Существующее
|
||||
непустое поле не перезаписывается. Ничего при этом не убирается и не
|
||||
переименовывается — клиентский `reasoning`/`reasoning_details` остаются на месте,
|
||||
поле просто дополняется. Тронуто только assistant-сообщение с непустым
|
||||
`tool_calls`; сообщения без `tool_calls` и `user`/`tool`/`system` не меняются.
|
||||
Если текста рассуждений нет вовсе — поле не добавляется, кроме случая
|
||||
`reasoning_empty_ok: true` (тогда пишется пустая строка `""`). Если
|
||||
`reasoning_field` не задан — тело не меняется вовсе.
|
||||
|
||||
```yaml
|
||||
providers:
|
||||
- id: opencode
|
||||
url: "https://opencode.ai/zen/go/v1"
|
||||
reasoning_field: reasoning_content # Console Go / deepseek в thinking-режиме
|
||||
# reasoning_empty_ok: true # опционально; дефолт false
|
||||
```
|
||||
|
||||
### Пример сборки тела (многослойный `patch`)
|
||||
|
||||
Берём модель `my-gpt` (из примера выше), маршрут уходит на апстрим
|
||||
@@ -296,9 +334,12 @@ data class ProviderConf(
|
||||
val id: String,
|
||||
val url: String,
|
||||
val key: String = "",
|
||||
val max_concurrency: Int? = null, // лимит по умолчанию для апстримов провайдера
|
||||
val patch: JsonObject? = null, // ко всем запросам провайдера
|
||||
val session_header: String? = null, // заголовок-сессия, считается из истории
|
||||
val max_concurrency: Int? = null,
|
||||
val patch: JsonObject? = null,
|
||||
val session_header: String? = null,
|
||||
val think_tags: String? = null,
|
||||
val reasoning_field: String? = null,
|
||||
val reasoning_empty_ok: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
Reference in New Issue
Block a user