Entropic 2.3.8
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
10namespace entropic {
11
20 : cb_(cb), ud_(ud) {}
21
30 raw_cb_ = cb;
31 raw_ud_ = ud;
32}
33
42static bool match_tag(const std::string& buf, bool& is_open) {
43 if (buf == "<think>") { is_open = true; return true; }
44 if (buf == "</think>") { is_open = false; return true; }
45 return false;
46}
47
55static int utf8_char_len(unsigned char byte) {
56 // Lookup: ASCII | continuation | 2-byte | 3-byte | 4-byte lead.
57 if (byte < 0x80) { return 1; }
58 int result = 0; // continuation byte default
59 if (byte >= 0xF0) { result = 4; }
60 else if (byte >= 0xE0) { result = 3; }
61 else if (byte >= 0xC0) { result = 2; }
62 return result;
63}
64
77void StreamThinkFilter::emit_utf8_safe(const char* data, size_t len) {
78 utf8_buf_.append(data, len);
79
80 // Find the last complete codepoint boundary
81 size_t safe = utf8_buf_.size();
82 if (safe == 0) { return; }
83
84 // Walk backward from end to find any incomplete trailing sequence
85 auto* buf = reinterpret_cast<const unsigned char*>(utf8_buf_.data());
86 for (size_t i = 1; i <= 4 && i <= safe; ++i) {
87 unsigned char c = buf[safe - i];
88 int expected = utf8_char_len(c);
89 if (expected > 0) {
90 // Found a lead byte at position (safe - i)
91 size_t available = i; // bytes from lead to end
92 if (available < static_cast<size_t>(expected)) {
93 // Incomplete sequence — emit up to lead byte
94 safe -= i;
95 }
96 break;
97 }
98 }
99
100 if (safe > 0) {
101 cb_(utf8_buf_.data(), safe, ud_);
102 }
103 utf8_buf_.erase(0, safe);
104}
105
119void StreamThinkFilter::process_byte(char c) {
120 if (tag_buf_.empty() && c != '<') {
121 if (!in_think_) { emit_utf8_safe(&c, 1); }
122 return;
123 }
124 tag_buf_ += c;
125 bool is_open = false;
126 if (match_tag(tag_buf_, is_open)) {
127 in_think_ = is_open;
128 tag_buf_.clear();
129 return;
130 }
131 if (tag_buf_.size() > 8) {
132 if (!in_think_) {
133 emit_utf8_safe(tag_buf_.data(), tag_buf_.size());
134 }
135 tag_buf_.clear();
136 }
137}
138
146void StreamThinkFilter::on_token(const char* chunk, size_t len) {
147 // Raw callback always gets everything (unfiltered, no UTF-8 alignment)
148 if (raw_cb_) { raw_cb_(chunk, len, raw_ud_); }
149
150 for (size_t i = 0; i < len; ++i) {
151 process_byte(chunk[i]);
152 }
153}
154
161 if (!tag_buf_.empty() && !in_think_) {
162 emit_utf8_safe(tag_buf_.data(), tag_buf_.size());
163 }
164 tag_buf_.clear();
165 // Flush any remaining UTF-8 buffer (may be incomplete at stream end)
166 if (!utf8_buf_.empty()) {
167 cb_(utf8_buf_.data(), utf8_buf_.size(), ud_);
168 utf8_buf_.clear();
169 }
170}
171
172} // 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.
static bool match_tag(const std::string &buf, bool &is_open)
Check if accumulated buffer matches a think tag.
Streaming filter that strips <think>...</think> blocks.