Repository navigation
litellm: reenviar el thinking historico a vLLM en el campo reasoning - #82
Merged
Merged
Conversation
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>
Contributor
Review automatica⏭️ Modelo no disponible, review omitida. contenido vacio (finish_reason='length') El perfil de computo de los Sparks se conmuta entre modelo |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
message["reasoning"](vllm/entrypoints/chat_utils.py). En suConversationMessage,reasoning_contentestá marcado "Deprecated": solo se escribe en la salida, de la entrada nunca se lee.reasoning_content— por el adaptadoropenai/y también por la traducción de/v1/messages, que además lo duplica enthinking_blocks.Medido
/tokenizesobre una secuencia de tools (no gasta GPU, renderiza la plantilla):La plantilla del modelo sí lo usa:
chat_template.jinja:111-117emite<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: Falsepreserva 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:
preserve_thinking:false)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 adaptadorhosted_vllm/— los vLLM locales van poropenai/, 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 listaSTRICT_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/revisionse 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