How to Enable Thinking Mode for Gemma 4 in LM Studio and OpenCode

These past few days, I’ve been tinkering with Gemma 4 locally.
Not just for the sake of messing around, but to test its capabilities and see if it could fit into a chatbot like OpenClaw: something that can run close to you, giving you more control over the environment, and not having to rely on an external provider for every interaction.
And of course, if I wanted to really test it, it wasn’t enough to see if it answered a few random prompts well. I also had to check how its thinking mode worked, because that’s often where you see if a local model can handle more complex flows.
Enabling thinking mode in a local model seems like one of those things that should just work out of the box.
You tick an option in LM Studio, load the model, connect it to OpenCode, and off you go.
Well, not quite.
The reality is that there are several different layers here, and if even one isn’t set up right, you’ll see one of these three things:
- the model doesn’t think;
- the model thinks but you don’t see it;
- or the model only thinks when you manually add a weird token to every prompt.
In my case, the goal was to get Gemma 4 26B working with thinking inside LM Studio and then use it from OpenCode, without having to type <|think|> manually every turn.
And that’s where the fun begins.
The Problem Isn’t in Just One Place
When you work with local models, we tend to talk about “enabling thinking” as if it’s a single option.
But really, there are several pieces:
- The model needs to know how to generate that reasoning channel.
- LM Studio needs to know how to interpret and parse it.
- The prompt template needs to trigger that behavior.
- OpenCode needs to send messages in a way that’s compatible with that template.
If any of those legs fails, you start getting false diagnoses.
For example:
- “This doesn’t work because OpenCode doesn’t respect the system prompt.”
- “No, the problem is LM Studio.”
- “No, the model doesn’t support reasoning.”
And most of the time, it’s not that simple.
The Important Part: LM Studio
In my case, most of the configuration was in LM Studio.
Inside My Models, for the Gemma 4 model, you need to tweak two things:
1. Reasoning Parsing
In the model’s advanced configuration:
Enabled:ONStart String:<|channel>thoughtEnd String:<channel|>
This tells LM Studio how to separate the final response content from the reasoning block.
If you don’t do this, the model might be thinking, but LM Studio won’t know how to tell the difference between the answer and the reasoning.
2. Prompt Template
This is usually where the real key is.
At the very top of the Gemma template, I added:
{%- set enable_thinking = true %}
And the template also injects <|think|> into the first system turn when enable_thinking is active.
The important part of the template was this:
{%- if enable_thinking is defined and enable_thinking -%}
{{- '<|think|>' -}}
{%- set ns.prev_message_type = 'think' -%}
{%- endif -%}
And at the end:
{%- if not enable_thinking | default(false) -%}
{{- '<|channel>thought\n<channel|>' -}}
{%- endif -%}
In plain English: the template is set up to open the thinking channel and so that LM Studio can then parse it.
The Detail That Cost Me Time
Here’s where things got tricky.
I assumed that if the template was already correct and LM Studio had Reasoning Parsing set up, then OpenCode just had to send its normal system prompt and everything would work.
But no.
The clue came from a very simple test:
- typing
hola, Gemma would respond normally; - typing
<|think|> hola, the thinking block would appear.
So, there was a real difference.
That taught me two things:
- The model did react to the token.
- The problem wasn’t just “LM Studio isn’t parsing.”
The First Thing to Check: Direct API
Before blaming OpenCode, it’s best to talk directly to LM Studio’s OpenAI-compatible API.
With a call like this:
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 would return something like:
{
"choices": [
{
"message": {
"role": "assistant",
"content": "OK",
"reasoning_content": "..."
}
}
]
}
In other words:
- the model was thinking;
- LM Studio was parsing it;
- the API was exposing
reasoning_content.
That rules out half the suspects.
So, What Was Happening with OpenCode?
What OpenCode does isn’t send a giant “raw” prompt.
What it sends to LM Studio is a conversation in OpenAI format:
{
"messages": [
{ "role": "system", "content": "..." },
{ "role": "user", "content": "hola" }
]
}
And here’s where the nuance really matters:
the trigger that was changing the behavior wasn’t the system prompt, but the content of the user’s turn.
When the message came in as:
hola
the behavior was one way.
When it came in as:
<|think|> hola
the behavior was different.
So the practical solution wasn’t “tweak OpenCode’s system prompt,” but automatically inject that prefix into user messages for that model.
The Clean Solution in OpenCode: a Plugin
Luckily, OpenCode has plugin hooks.
Instead of forcing myself to type <|think|> in every prompt, I created a global plugin at:
~/.config/opencode/plugins/gemma-think.js
With this content:
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}`;
},
};
};
The advantage of doing it this way:
- it only affects that model;
- it doesn’t mess up others like Qwen;
- I don’t have to remember to type the token;
- I don’t have to replace the agent’s base prompt;
- and the integration stays completely local.
How to Check That It’s Really Working
Don’t just trust your gut here.
The right thing to do is check LM Studio’s logs and see what it’s actually receiving.
In my case, after the plugin, OpenCode’s request looked like this:
{
"role": "user",
"content": "<|think|> hola\n"
}
And I also checked that for another model, like Qwen, it was still just:
{
"role": "user",
"content": "hola\n"
}
That confirmed:
- the hook was working;
- it only affected Gemma;
- the token wasn’t accidentally getting into all models.
The Most Common Conceptual Mistake Here
I think the easiest mistake to make in an integration like this is to think:
“If there’s reasoning, then the system prompt should be enough.”
And not necessarily.
There are models and templates where the behavior changes depending on:
- the first system turn;
- the format of the last turn;
- an explicit activation token;
- or a combination of several things.
That’s why, if something doesn’t add up, my recommendation is:
- First, check LM Studio with
curl. - Make sure
reasoning_contentshows up. - Look in the logs to see what OpenCode is actually sending.
- Don’t assume the right place for the trigger is the system prompt.
A lot of times, it isn’t.
Conclusion
If you want to enable Gemma 4’s thinking mode with LM Studio and OpenCode, the solution isn’t just “tick a box.”
You need to:
- properly configure
Reasoning Parsingin LM Studio; - adjust the model’s prompt template;
- check that the API returns
reasoning_content; - and, in the case of OpenCode, make sure the trigger gets where it needs to go.
In my case, that trigger was in the user message, not the system prompt.
And the cleanest way to solve it was an OpenCode plugin that adds <|think|> automatically, but only for gemma-4-26b-a4b-it.
Which, by the way, is exactly the kind of detail that can cost you an entire afternoon if you don’t check the logs.
And yes, sometimes the bug isn’t where you think it is.
How I Reorganized All My Repos with AI (and Refreshed My GitHub Profiles Along the Way)
What is Loop Engineering? A practical example with Codex