Cómo activar el thinking mode de Gemma 4 en LM Studio y OpenCode

A
Antonio Leiva
7 min lectura

Estos días atrás he estado trasteando con Gemma 4 en local.

No tanto por cacharrear sin más, sino para probar sus capacidades y ver si podía encajar en un chatbot estilo OpenClaw: algo que pueda correr cerca de ti, con más control sobre el entorno, y sin depender siempre de un proveedor externo para cada interacción.

Y claro, si quería probarlo en serio, no bastaba con ver si respondía bien a cuatro prompts sueltos. También tenía que comprobar cómo funcionaba su thinking mode, porque ahí es donde muchas veces se nota si un modelo local puede aguantar flujos algo más complejos.

Activar el thinking mode en un modelo local parece de esas cosas que deberían funcionar a la primera.

Marcas una opción en LM Studio, cargas el modelo, lo conectas a OpenCode y a correr.

Pues no.

La realidad es que aquí hay varias capas distintas, y si una sola no está bien configurada, lo que ves es una de estas tres cosas:

  • el modelo no piensa;
  • el modelo piensa pero tú no lo ves;
  • o el modelo piensa solo cuando le metes un token raro a mano en cada prompt.

En mi caso, el objetivo era hacer que Gemma 4 26B funcionara con thinking dentro de LM Studio y usarlo después desde OpenCode, sin tener que escribir <|think|> manualmente en cada turno.

Y ahí es donde empieza lo divertido.

El problema no está en un único sitio

Cuando trabajas con modelos locales, tendemos a hablar de “activar el thinking” como si fuera una sola opción.

Pero realmente hay varias piezas:

  1. El modelo tiene que saber generar ese canal de razonamiento.
  2. LM Studio tiene que saber interpretarlo y parsearlo.
  3. El prompt template tiene que disparar ese comportamiento.
  4. OpenCode tiene que enviar los mensajes de una forma compatible con ese template.

Si una de esas patas falla, empiezan los falsos diagnósticos.

Por ejemplo:

  • “Esto no funciona porque OpenCode no respeta el system prompt”.
  • “No, el problema es LM Studio”.
  • “No, el modelo no soporta reasoning”.

Y la mayoría de veces no es tan simple.

La parte importante: LM Studio

En mi caso, el grueso de la configuración estaba en LM Studio.

Dentro de My Models, sobre el modelo de Gemma 4, hay que tocar dos cosas:

1. Reasoning Parsing

En la configuración avanzada del modelo:

  • Enabled: ON
  • Start String: <|channel>thought
  • End String: <channel|>

Esto es lo que le dice a LM Studio cómo separar el contenido final de la respuesta del bloque de razonamiento.

Si no haces esto, el modelo puede estar pensando, pero LM Studio no sabrá distinguir entre lo que es respuesta y lo que es razonamiento.

2. Prompt Template

Aquí es donde suele estar la clave real.

En el template de Gemma añadí arriba del todo:

{%- set enable_thinking = true %}

Y además el template inyecta <|think|> en el primer turno del sistema cuando enable_thinking está activo.

La parte importante del template era esta:

{%- if enable_thinking is defined and enable_thinking -%}
    {{- '<|think|>' -}}
    {%- set ns.prev_message_type = 'think' -%}
{%- endif -%}

Y al final:

{%- if not enable_thinking | default(false) -%}
    {{- '<|channel>thought\n<channel|>' -}}
{%- endif -%}

Traducido al castellano: el template está preparado para abrir el canal de pensamiento y para que LM Studio luego pueda parsearlo.

El detalle que me hizo perder tiempo

Aquí vino la parte engañosa.

Yo daba por hecho que, si el template ya estaba bien y LM Studio tenía configurado el Reasoning Parsing, entonces OpenCode solo tenía que mandar su system prompt normal y todo iría rodado.

Pero no.

La pista vino al hacer una prueba muy tonta:

  • escribiendo hola, Gemma respondía normal;
  • escribiendo <|think|> hola, aparecía el bloque de thinking.

O sea, había una diferencia real.

Eso me enseñó dos cosas:

  1. El modelo sí reaccionaba al token.
  2. El problema no era simplemente “LM Studio no está parseando”.

Lo primero que conviene verificar: la API directa

Antes de culpar a OpenCode, lo mejor es hablar directamente con la API OpenAI-compatible de LM Studio.

Con una llamada como esta:

curl -s http://127.0.0.1:1234/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "gemma-4-26b-a4b-it",
    "messages": [
      { "role": "system", "content": "You are helpful." },
      { "role": "user", "content": "Reply with exactly OK" }
    ],
    "stream": false
  }'

LM Studio me devolvía algo así:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "OK",
        "reasoning_content": "..."
      }
    }
  ]
}

Es decir:

  • el modelo sí estaba pensando;
  • LM Studio sí lo estaba parseando;
  • la API sí exponía reasoning_content.

Eso descarta media lista de sospechosos.

Entonces, ¿qué estaba pasando con OpenCode?

Lo que hace OpenCode no es mandar un prompt gigante “crudo”.

Lo que manda a LM Studio es una conversación en formato OpenAI:

{
  "messages": [
    { "role": "system", "content": "..." },
    { "role": "user", "content": "hola" }
  ]
}

Y aquí es donde el matiz importa mucho:

el trigger que estaba cambiando el comportamiento no era el system prompt, sino el contenido del turno del usuario.

Cuando el mensaje llegaba como:

hola

el comportamiento era uno.

Cuando llegaba como:

<|think|> hola

el comportamiento era otro.

Así que la solución práctica no era “tocar el system prompt de OpenCode”, sino inyectar ese prefijo automáticamente en los mensajes del usuario para ese modelo.

La solución limpia en OpenCode: un plugin

Por suerte, OpenCode tiene hooks de plugin.

En lugar de obligarme a escribir <|think|> en cada prompt, creé un plugin global en:

~/.config/opencode/plugins/gemma-think.js

Con este contenido:

export const GemmaThinkPlugin = async () => {
  return {
    "experimental.chat.messages.transform": async (_input, output) => {
      const messages = output.messages;
      if (!Array.isArray(messages) || messages.length === 0) return;

      const last = messages[messages.length - 1];
      if (!last || last.info.role !== "user") return;

      const model = last.info.model;
      if (!model || model.providerID !== "lmstudio" || model.modelID !== "gemma-4-26b-a4b-it") {
        return;
      }

      const firstTextPart = last.parts.find((part) => part.type === "text");
      if (!firstTextPart) return;

      if (typeof firstTextPart.text !== "string") return;
      if (firstTextPart.text.startsWith("<|think|>")) return;

      firstTextPart.text = `<|think|> ${firstTextPart.text}`;
    },
  };
};

La ventaja de hacerlo así:

  • solo afecta a ese modelo;
  • no ensucia otros como Qwen;
  • no tengo que acordarme de escribir el token;
  • no tengo que reemplazar el prompt base del agente;
  • y la integración sigue siendo completamente local.

Cómo verificar que realmente está funcionando

Aquí no hay que fiarse de sensaciones.

Lo correcto es mirar los logs de LM Studio y comprobar qué está recibiendo realmente.

En mi caso, después del plugin, el request de OpenCode pasó a verse así:

{
  "role": "user",
  "content": "<|think|> hola\n"
}

Y además comprobé que para otro modelo, como Qwen, seguía llegando simplemente:

{
  "role": "user",
  "content": "hola\n"
}

Eso me confirmaba que:

  1. el hook estaba funcionando;
  2. solo afectaba a Gemma;
  3. el token no estaba entrando por accidente en todos los modelos.

El error conceptual más común aquí

Creo que el error más fácil de cometer en una integración como esta es pensar:

“Si hay reasoning, entonces el system prompt debería ser suficiente”.

Y no necesariamente.

Hay modelos y templates donde el comportamiento cambia según:

  • el primer turno del sistema;
  • el formato del último turno;
  • un token explícito de activación;
  • o una combinación de varias cosas.

Por eso, si algo no cuadra, mi recomendación es esta:

  1. Verifica primero LM Studio con curl.
  2. Comprueba que aparece reasoning_content.
  3. Mira en logs qué está enviando realmente OpenCode.
  4. No des por hecho que el sitio correcto para el trigger es el system prompt.

Muchas veces no lo es.

Conclusión

Si quieres activar el thinking mode de Gemma 4 con LM Studio y OpenCode, la solución no es solo “marcar una casilla”.

Necesitas:

  • configurar bien el Reasoning Parsing en LM Studio;
  • ajustar el prompt template del modelo;
  • verificar que la API devuelve reasoning_content;
  • y, en el caso de OpenCode, asegurarte de que el trigger llega donde tiene que llegar.

En mi caso, ese trigger estaba en el mensaje del usuario, no en el system prompt.

Y la forma más limpia de resolverlo fue un plugin de OpenCode que añade <|think|> automáticamente solo para gemma-4-26b-a4b-it.

Que, dicho sea de paso, es justo el tipo de detalle que te hace perder una tarde entera si no inspeccionas los logs.

Y sí, a veces el bug no está donde tú crees.

Recursos de expertos para la solución de problemas

Ver todos