main
dart 57 lines 1.97 KB
Raw
1 typedef OllamaTextPart = ({String text, bool isReasoning});
2
3 /// Legacy Ollama models may return a leading `<think>` block in content.
4 /// Native `message.thinking` takes precedence. Only a leading block is parsed,
5 /// so tags quoted later in an ordinary answer remain ordinary answer text.
6 class OllamaReasoningDecoder {
7 String _pending = '';
8 bool _checkedPrefix = false;
9 bool _inReasoning = false;
10
11 List<OllamaTextPart> add(String text, {required bool nativeThinking}) {
12 _pending += text;
13 if (nativeThinking) {
14 _checkedPrefix = true;
15 _inReasoning = false;
16 return flush();
17 }
18 if (!_checkedPrefix) {
19 final trimmed = _pending.trimLeft();
20 const opening = '<think>';
21 if (opening.startsWith(trimmed) && trimmed != opening) return [];
22 _checkedPrefix = true;
23 if (trimmed.startsWith(opening)) {
24 _inReasoning = true;
25 _pending = trimmed.substring(opening.length);
26 }
27 }
28 if (!_inReasoning) return flush();
29
30 const closing = '</think>';
31 final end = _pending.indexOf(closing);
32 if (end >= 0) {
33 final reasoning = _pending.substring(0, end);
34 final answer = _pending.substring(end + closing.length);
35 _pending = '';
36 _inReasoning = false;
37 return [
38 if (reasoning.isNotEmpty) (text: reasoning, isReasoning: true),
39 if (answer.isNotEmpty) (text: answer, isReasoning: false),
40 ];
41 }
42 // Keep only a possible partial closing tag across stream boundaries.
43 var keep = 0;
44 for (var length = 1; length < closing.length; length++) {
45 if (_pending.endsWith(closing.substring(0, length))) keep = length;
46 }
47 final ready = _pending.substring(0, _pending.length - keep);
48 _pending = _pending.substring(_pending.length - keep);
49 return [if (ready.isNotEmpty) (text: ready, isReasoning: true)];
50 }
51
52 List<OllamaTextPart> flush() {
53 final text = _pending;
54 _pending = '';
55 return [if (text.isNotEmpty) (text: text, isReasoning: _inReasoning)];
56 }
57 }