Skip to content

litellm: reenviar el thinking historico a vLLM en el campo reasoning - #82

Merged
pocharlies merged 1 commit into
mainfrom
forward-reasoning-vllm
Sep 14, 2026
Merged

pocharlies merged 1 commit into
mainfrom
forward-reasoning-vllm

Conversation

@pocharlies

Copy link
Copy Markdown
Collaborator

Qué pasa hoy

El razonamiento que el cliente sí conserva en el historial llega al backend y se descarta en silencio, antes de la plantilla y sin error. El modelo vuelve a razonar lo ya razonado en cada paso de herramienta.

La causa no es LiteLLM: es un nombre de campo.

  • vLLM lee el thinking previo de message["reasoning"] (vllm/entrypoints/chat_utils.py). En su ConversationMessage, reasoning_content está marcado "Deprecated": solo se escribe en la salida, de la entrada nunca se lee.
  • LiteLLM emite reasoning_content — por el adaptador openai/ y también por la traducción de /v1/messages, que además lo duplica en thinking_blocks.

Medido

/tokenize sobre una secuencia de tools (no gasta GPU, renderiza la plantilla):

sin thinking histórico               321 tok
campo 'reasoning_content' (LiteLLM)  321 tok   ← se descarta
campo 'reasoning'  (lo que vLLM lee) 472 tok   ← llega

La plantilla del modelo sí lo usa: chat_template.jinja:111-117 emite <think>…</think> en cuanto el campo llega con el nombre bueno. El único eslabón roto era el nombre.

Por qué acotado al turno en curso

chat_template_kwargs.preserve_thinking: False preserva solo el thinking posterior al último mensaje de usuario — la secuencia de herramientas en marcha, que es lo que recomienda Qwen. Sin ese kwarg se preservaría el de toda la conversación y el contexto de una sesión agéntica larga crece sin tope.

El precio está medido y es el prefix cache:

tokens turno 1 prefijo compartido al llegar un mensaje nuevo
hoy 351 100 % (recomputa 1 tok)
este PR (preserve_thinking:false) 953 30 % (recomputa 666 tok)
todo el historial 953 100 % (recomputa 1 tok)

Se paga una vez por turno de usuario, no por paso de herramienta: dentro del turno el prefijo sigue creciendo de forma monótona. A cambio, el contexto no se infla de forma acumulativa (turno 2: 358 tok, casi igual que hoy).

Relación con el PR 41050 de upstream

No es ese arreglo. BerriAI/litellm#41050 reenvía reasoning_content, que es justo el nombre que vLLM ya no lee. Y aquí no aplicaría de todas formas: ningún modelo de este proxy usa el adaptador hosted_vllm/ — los vLLM locales van por openai/, que ya conserva el campo. Comprobado reproduciendo ambos adaptadores contra la librería del pod.

Alcance

Acotado a FORWARD_REASONING_MODELS = los dos alias del backend qwen38-flash-next, el mismo par que ya lista STRICT_LEADING_SYSTEM_MODELS (mismo backend, mismo template). Un test de contrato exige que las dos listas no se desincronicen.

La llamada va con el modelo resuelto, no con el alias pedido: con el alias sería un no-op para tooling/high/max.

Tests

tests/test_forward_reasoning_contract.py, 10 casos que ejecutan la función real extraída del manifiesto (no una copia). Existe porque el fallo es mudo: si alguien renombra el campo o retira el kwarg, nada peta y el único síntoma es que el modelo vuelve a razonar lo ya razonado.

Suite completa: 321 pasan. Queda 1 rojo, test_openrouter_access_contract, preexistente en main limpio (migración de ExternalSecrets a 1Password, 11bfdaf) y ajeno a este cambio.

Nota de despliegue

config.k8s.e-dani.com/revision se recalcula: el hook vive en un ConfigMap que el proceso no relee en caliente. Venía ya desactualizada de antes de este cambio.

🤖 Generated with Claude Code

vLLM lee el razonamiento previo de `message["reasoning"]` y NO de
`reasoning_content`: en su ConversationMessage ese nombre esta marcado
"Deprecated" y solo se ESCRIBE en la salida, de la entrada nunca se lee
(vllm/entrypoints/chat_utils.py). LiteLLM emite `reasoning_content` -- por el
adaptador openai/ y tambien por la traduccion de /v1/messages, que ademas lo
duplica en `thinking_blocks`. El razonamiento que el cliente SI conserva llegaba
al backend y se descartaba en silencio, antes de la plantilla y sin error, asi
que el modelo volvia a razonar lo ya razonado en cada paso de herramienta.

Medido con /tokenize sobre una secuencia de tools: 321 tokens sin thinking ==
321 con `reasoning_content` -> 472 con `reasoning`. El template del modelo si lo
usa (chat_template.jinja:111-117 emite <think>...</think> en cuanto el campo
llega bien); el unico eslabon roto era el nombre.

Acotado al turno EN CURSO con `chat_template_kwargs.preserve_thinking: False`,
que es lo que recomienda Qwen: solo se preserva el thinking posterior al ultimo
mensaje de usuario -- la secuencia de herramientas en marcha. Sin ese kwarg se
preservaria el de toda la conversacion y el contexto de una sesion agentica
larga crece sin tope.

El precio de acotarlo esta medido y es el prefix cache: al llegar un mensaje
nuevo del usuario, el thinking del turno anterior desaparece del prompt y el
prefijo compartido cae del 100% al 30% en la secuencia de prueba (~666 tokens a
recomputar). Se paga una vez por turno de usuario y no por paso de herramienta:
dentro del turno el prefijo sigue creciendo de forma monotona.

No es el PR 41050 de upstream (`forward_reasoning_content` para hosted_vllm):
ese reenvia justo el nombre que vLLM ya no lee, y aqui no aplicaria de todas
formas porque ningun modelo de este proxy usa el adaptador hosted_vllm/ -- los
vLLM locales van por openai/, que ya conserva el campo.

La anotacion config.k8s.e-dani.com/revision se recalcula porque el hook vive en
un ConfigMap que el proceso no relee en caliente. Venia ya desactualizada de
antes de este cambio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Review automatica

⏭️ Modelo no disponible, review omitida. contenido vacio (finish_reason='length')

El perfil de computo de los Sparks se conmuta entre llm-tp y creative; en creative el modelo no tiene endpoints. El check queda en verde a proposito.

modelo tooling · revision no bloqueante

@pocharlies
pocharlies merged commit 44e519b into main Sep 14, 2026
@pocharlies
pocharlies deleted the forward-reasoning-vllm branch September 14, 2026 14:27
pocharlies added a commit that referenced this pull request Sep 14, 2026
Correccion de la decision tomada esta misma manana en el PR #82. Ahi se mandaba
`chat_template_kwargs.preserve_thinking: False` para "acotar el thinking al turno
en curso". Va en contra de lo que recomienda el fabricante para ESTE modelo.

El model card de Qwen3.8-Flash-Next, seccion "Disable Preserved Thinking":

  "By default, Qwen3.8-Flash-Next retains thinking blocks from all historical
   messages [...] This behavior, known as preserved thinking, ensures full
   context continuity and is especially beneficial for agent scenarios where
   decision consistency and reduced redundant reasoning are critical. It also
   improves KV cache utilization"

y presenta `preserve_thinking: False` como "if you prefer to retain only the
thinking blocks from the latest user message" -- una preferencia, no la
recomendacion. En el 27B la recomendacion es la contraria, pero ese modelo lo
sirve SGLang y no esta en FORWARD_REASONING_MODELS: son dos cosas distintas y el
#82 las mezclo.

Lo contraintuitivo, y el motivo de haberlo hecho mal: `False` parece que ahorra
contexto y lo que hace es TIRAR la cache. Ya estaba medido en el propio #82 y se
interpreto al reves -- con el default el prefijo compartido entre turnos es del
100% (recomputa 1 token) y con `False` cae al 30% (~666 tokens a recomputar),
porque el thinking de los turnos anteriores desaparece del prompt
RETROACTIVAMENTE y rompe el prefijo. Los tokens extra de un prefijo estable
salen de la cache; los de un prefijo roto hay que recomputarlos. Este backend
arrastra ~81.000 tokens de prompt por peticion: la cache no es un detalle. Es
exactamente el "improves KV cache utilization" del model card.

El renombrado `reasoning_content` -> `reasoning`, que es lo que de verdad
arreglaba el fallo mudo, se queda igual. El hook pasa a tocar solo `messages` y
ya no escribe en extra_body.

Co-authored-by: Daniel Ibáñez <info@e-dani.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant