Entropic 2.11.1
Local-first agentic inference engine
Loading...
Searching...
No Matches
stream_think_filter.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
9
10#include <algorithm>
11#include <utility>
12
13namespace entropic {
14
23 : cb_(cb), ud_(ud) {}
24
35 std::string open_marker,
36 std::string close_marker)
37 : cb_(cb), ud_(ud),
38 open_marker_(std::move(open_marker)),
39 close_marker_(std::move(close_marker)),
40 max_marker_len_(std::max(open_marker_.size(), close_marker_.size())) {}
41
50 raw_cb_ = cb;
51 raw_ud_ = ud;
52}
53
62bool StreamThinkFilter::match_tag(const std::string& buf, bool& is_open) const {
63 if (buf == open_marker_) { is_open = true; return true; }
64 if (buf == close_marker_) { is_open = false; return true; }
65 return false;
66}
67
75static int utf8_char_len(unsigned char byte) {
76 // Lookup: ASCII | continuation | 2-byte | 3-byte | 4-byte lead.
77 if (byte < 0x80) { return 1; }
78 int result = 0; // continuation byte default
79 if (byte >= 0xF0) { result = 4; }
80 else if (byte >= 0xE0) { result = 3; }
81 else if (byte >= 0xC0) { result = 2; }
82 return result;
83}
84
97void StreamThinkFilter::emit_utf8_safe(const char* data, size_t len) {
98 utf8_buf_.append(data, len);
99
100 // Find the last complete codepoint boundary
101 size_t safe = utf8_buf_.size();
102 if (safe == 0) { return; }
103
104 // Walk backward from end to find any incomplete trailing sequence
105 auto* buf = reinterpret_cast<const unsigned char*>(utf8_buf_.data());
106 for (size_t i = 1; i <= 4 && i <= safe; ++i) {
107 unsigned char c = buf[safe - i];
108 int expected = utf8_char_len(c);
109 if (expected > 0) {
110 // Found a lead byte at position (safe - i)
111 size_t available = i; // bytes from lead to end
112 if (available < static_cast<size_t>(expected)) {
113 // Incomplete sequence — emit up to lead byte
114 safe -= i;
115 }
116 break;
117 }
118 }
119
120 if (safe > 0) {
121 cb_(utf8_buf_.data(), safe, ud_);
122 }
123 utf8_buf_.erase(0, safe);
124}
125
139void StreamThinkFilter::process_byte(char c) {
140 // Only start buffering on a byte that could begin either delimiter.
141 if (tag_buf_.empty() && c != open_marker_[0] && c != close_marker_[0]) {
142 if (!in_think_) { emit_utf8_safe(&c, 1); }
143 return;
144 }
145 tag_buf_ += c;
146 bool is_open = false;
147 if (match_tag(tag_buf_, is_open)) {
148 in_think_ = is_open;
149 tag_buf_.clear();
150 return;
151 }
152 // Past the longest delimiter this family uses, it cannot be one.
153 if (tag_buf_.size() > max_marker_len_) {
154 if (!in_think_) {
155 emit_utf8_safe(tag_buf_.data(), tag_buf_.size());
156 }
157 tag_buf_.clear();
158 }
159}
160
168void StreamThinkFilter::on_token(const char* chunk, size_t len) {
169 // Raw callback always gets everything (unfiltered, no UTF-8 alignment)
170 if (raw_cb_) { raw_cb_(chunk, len, raw_ud_); }
171
172 for (size_t i = 0; i < len; ++i) {
173 process_byte(chunk[i]);
174 }
175}
176
183 if (!tag_buf_.empty() && !in_think_) {
184 emit_utf8_safe(tag_buf_.data(), tag_buf_.size());
185 }
186 tag_buf_.clear();
187 // Flush any remaining UTF-8 buffer (may be incomplete at stream end)
188 if (!utf8_buf_.empty()) {
189 cb_(utf8_buf_.data(), utf8_buf_.size(), ud_);
190 utf8_buf_.clear();
191 }
192}
193
194} // namespace entropic
void set_raw_callback(TokenCallback cb, void *ud)
Set optional raw callback (receives ALL tokens unfiltered).
void on_token(const char *chunk, size_t len)
Process a chunk of tokens.
void flush()
Flush any buffered partial tag content.
StreamThinkFilter(TokenCallback cb, void *ud)
Construct with consumer callback.
Activate model on GPU (WARM → ACTIVE).
static int utf8_char_len(unsigned char byte)
Count expected bytes in a UTF-8 sequence from lead byte.
void(*)(const char *, size_t, void *) TokenCallback
Token callback type matching the C API signature.
Streaming filter that strips a model family's reasoning blocks.