From b9d7ea7ca80ec559082d93b706441fbfc12d12d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ib=C3=A1=C3=B1ez?= Date: Mon, 14 Sep 2026 17:17:13 +0200 Subject: [PATCH] litellm: dejar `preserve_thinking` al default en qwen38-flash-next 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: Claude Opus 5 (1M context) --- k8s/manifest.yaml | 55 ++++++++++++------------ tests/test_forward_reasoning_contract.py | 30 ++++++++----- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/k8s/manifest.yaml b/k8s/manifest.yaml index 5a6cc70..399846c 100644 --- a/k8s/manifest.yaml +++ b/k8s/manifest.yaml @@ -2944,6 +2944,10 @@ data: # arregla esto: reenvia justo el nombre que vLLM ya no lee. Y aqui no aplica # de todas formas -- ningun modelo de este proxy usa el adaptador # hosted_vllm/, todos los vLLM locales van por openai/, que ya lo conserva. + # + # Este renombrado es lo UNICO que hace falta. El `preserve_thinking` se deja + # al default del modelo (= preservar todo el historial), que es lo que + # recomienda el model card de Flash-Next; ver el docstring de abajo. FORWARD_REASONING_MODELS = frozenset( {"qwen38-flash-next", "qwen38-flash-next-uncensored"} ) @@ -2951,21 +2955,27 @@ data: def _forward_reasoning_to_vllm(data): """Renombra `reasoning_content` -> `reasoning` en los assistant del historial. - Acotado al turno EN CURSO con `preserve_thinking: False`, que es lo que - recomienda Qwen: el template (chat_template.jinja:116) solo emite el - de los mensajes posteriores al ultimo `user`, o sea la secuencia - de herramientas en marcha. Sin ese kwarg preservaria el thinking de toda - la conversacion, y entonces 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 al 30% (recomputa ~666 tok en la - secuencia de prueba) en vez del 100% de hoy. Se paga una vez por turno - de usuario, no por paso de herramienta -- dentro del turno el prefijo - sigue creciendo de forma monotona y la cache funciona igual. - - Respeta al cliente: si ya mando `preserve_thinking`, no se pisa. + NO toca `preserve_thinking`: se deja el DEFAULT del modelo, que preserva + el thinking de TODOS los mensajes historicos. Es lo que pide el model + card de Qwen3.8-Flash-Next, literalmente: preserved thinking "is + especially beneficial for agent scenarios where decision consistency and + reduced redundant reasoning are critical. It also improves KV cache + utilization". `preserve_thinking: False` esta ahi como preferencia del + que quiera solo el ultimo turno, no como recomendacion. + + Ojo, que es contraintuitivo y ya se hizo mal una vez (14-09-2026): poner + `False` para "acotar al turno en curso" parece que ahorra contexto y en + realidad TIRA la cache. Medido: con el default el prefijo compartido + entre turnos es del 100% (recomputa 1 token); 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. Y este backend arrastra ~81.000 tokens de prompt + por peticion, asi que la cache no es un detalle. + + En el 27B la recomendacion es la contraria, pero ese modelo lo sirve + SGLang y no esta en FORWARD_REASONING_MODELS: no mezclar las dos. + `thinking_blocks` se retira porque es el MISMO contenido duplicado por la traduccion de /v1/messages y vLLM no lo lee: solo es peso en el JSON. """ @@ -2987,15 +2997,6 @@ data: moved += 1 if not moved: return False - body = data.get("extra_body") - if not isinstance(body, dict): - body = {} - data["extra_body"] = body - kwargs = body.get("chat_template_kwargs") - if not isinstance(kwargs, dict): - kwargs = {} - body["chat_template_kwargs"] = kwargs - kwargs.setdefault("preserve_thinking", False) router_log.info( "forward_reasoning: %d mensaje(s) con thinking historico -> campo `reasoning` (modelo %s)", moved, data.get("model"), @@ -4261,8 +4262,8 @@ data: # Mismo motivo para estar aqui abajo que los dos de arriba: el # renombrado de `reasoning_content` -> `reasoning` depende del # BACKEND que va a atender, no del alias que pidio el cliente, y - # solo aqui data["model"] es el resuelto. Va ANTES del sello porque - # escribe en extra_body (chat_template_kwargs.preserve_thinking). + # solo aqui data["model"] es el resuelto. Solo toca `messages`: el + # `preserve_thinking` se deja al DEFAULT del modelo a proposito. if data.get("model") in FORWARD_REASONING_MODELS: _forward_reasoning_to_vllm(data) # DESPUES de todo lo que escribe en extra_body, y por eso va aqui: si @@ -5913,7 +5914,7 @@ spec: # al arrancar; que kubelet refresque los ficheros montados no actualiza el # router en memoria. El contrato y el --fix viven en # tests/test_configmap_revision_bump_contract.py. - config.k8s.e-dani.com/revision: config-37c66a4ac723 + config.k8s.e-dani.com/revision: config-c6d4d316ba54 active-requests-api/revision: "20260726-request-history-trace" spec: enableServiceLinks: false diff --git a/tests/test_forward_reasoning_contract.py b/tests/test_forward_reasoning_contract.py index b4a39c3..fb28fdf 100644 --- a/tests/test_forward_reasoning_contract.py +++ b/tests/test_forward_reasoning_contract.py @@ -109,11 +109,20 @@ def test_renombra_reasoning_content_al_campo_que_vllm_lee(): assert data["messages"][1]["reasoning"] == "PENSADO ANTES" -def test_acota_el_thinking_al_turno_en_curso(): - """`preserve_thinking: False` = solo el thinking posterior al ultimo `user`. - - Es lo que recomienda Qwen y lo que impide que el contexto de una sesion - agentica larga crezca sin tope acumulando el razonamiento de cada turno. +def test_no_toca_preserve_thinking_deja_el_default_del_modelo(): + """El default de Flash-Next preserva el thinking de TODO el historial. + + El model card lo dice expresamente: preserved thinking "is especially + beneficial for agent scenarios where decision consistency and reduced + redundant reasoning are critical. It also improves KV cache utilization". + `preserve_thinking: False` es una preferencia para quien solo quiera el + ultimo turno, no la recomendacion. + + El 14-09-2026 este hook mandaba `False` creyendo que "acotar al turno en + curso" ahorraba contexto. Lo que hace es TIRAR la cache: el thinking de los + turnos anteriores desaparece del prompt retroactivamente y el prefijo + compartido cae del 100% al 30%. Con ~81.000 tokens de prompt por peticion en + este backend, eso es mucho mas caro que los tokens que ahorra. """ forward = _load_forwarder() data = _data( @@ -122,18 +131,19 @@ def test_acota_el_thinking_al_turno_en_curso(): {"role": "tool", "tool_call_id": "c1", "content": "ok"}, ) forward(data) - assert data["extra_body"]["chat_template_kwargs"]["preserve_thinking"] is False + assert "extra_body" not in data -def test_no_pisa_el_preserve_thinking_del_cliente(): +def test_no_pisa_los_chat_template_kwargs_del_cliente(): + """Si el cliente los mando, salen intactos: el hook solo toca `messages`.""" forward = _load_forwarder() data = _data( {"role": "user", "content": "haz algo"}, _assistant(reasoning_content="PENSADO ANTES"), ) - data["extra_body"] = {"chat_template_kwargs": {"preserve_thinking": True}} + data["extra_body"] = {"chat_template_kwargs": {"preserve_thinking": False}} forward(data) - assert data["extra_body"]["chat_template_kwargs"]["preserve_thinking"] is True + assert data["extra_body"] == {"chat_template_kwargs": {"preserve_thinking": False}} def test_retira_los_thinking_blocks_duplicados(): @@ -155,7 +165,7 @@ def test_retira_los_thinking_blocks_duplicados(): def test_es_noop_sin_thinking_en_el_historial(): - """Sin razonamiento previo no se toca extra_body: ni kwarg, ni cambio de prompt.""" + """Sin razonamiento previo no se toca nada.""" forward = _load_forwarder() data = _data( {"role": "user", "content": "haz algo"},