Thinking mode exposes the model's internal reasoning process as a separate response channel, so you can show a "thinking" bubble in your UI before the final answer.
Supported models#
- Gemma 4 (E2B, E4B) —
ModelType.gemma4 - DeepSeek R1 —
ModelType.deepSeek -
Qwen3 0.6B —
ModelType.qwen3; generates thinking by default, tags are stripped whenisThinking: false.
Enable it with isThinking: true and the matching ModelType.
Handling thinking responses#
The model emits a ThinkingResponse (with response.content) for its reasoning,
alongside regular TextResponse tokens for the final answer:
chat.generateChatResponseAsync().listen((response) {
if (response is ThinkingResponse) {
// Model's reasoning process
print('Thinking: ${response.content}');
_showThinkingBubble(response.content);
} else if (response is TextResponse) {
// The final answer
print('Text token: ${response.token}');
}
});
You can also create a thinking message manually:
final thinkingMessage = Message.thinking(text: "Let me analyze this problem...");
Platform support#
| Platform | Thinking Mode |
|---|---|
| Android | ✅ Full |
| iOS | ✅ Full |
| Desktop (macOS/Windows/Linux) | ✅ Full |
| Web | ⚠️ Qwen3 / DeepSeek R1 only |
Advanced: ModelThinkingFilter#
For custom inference implementations, ModelThinkingFilter cleans model outputs —
removing model-specific tokens. This is handled automatically by the chat API,
but is available if you need it:
import 'package:flutter_gemma/core/extensions.dart';
String cleanedResponse = ModelThinkingFilter.cleanResponse(
rawResponse,
isThinking: true,
modelType: ModelType.deepSeek,
fileType: ModelFileType.task,
);
// It removes the reasoning blocks (for these model types even when
// isThinking is false):
// - <think>...</think> (DeepSeek, Qwen, Qwen3)
// - <|channel>thought\n...<channel|> (Gemma 3 / Gemma 4 types)
// and trims whitespace. Turn markers (<end_of_turn>, <|im_end|>) are stripped
// only for .bin / .tflite files — on .task and .litertlm the runtime already
// ends the turn.