chore: snapshot main sync

This commit is contained in:
2026-06-17 20:08:31 +00:00
parent b18de2a331
commit 8bd32537cf
354 changed files with 10208 additions and 9230 deletions

View File

@@ -0,0 +1,100 @@
// ThinkSplitter — peels inline <think>...</think> reasoning out of streamed text
// content. Some local models (QwQ, DeepSeek-R1 distills, MiniMax) served raw
// emit their chain-of-thought inline in the assistant `content` channel rather
// than on a structured reasoning channel; BooCode's stream adapter otherwise
// treats that as ordinary prose. This splitter routes the reasoning span to the
// reasoning accumulator and passes the rest through unchanged.
//
// Ported from deepseek-reasonix internal/provider/openai/think.go. Two
// guarantees make it safe to run on every text delta:
// 1. It only ARMS if the turn's content begins with <think> (after leading
// whitespace), so an answer that merely mentions the tag is never hijacked.
// 2. For any content that does not start with <think> it degrades to a
// verbatim pass-through (a no-op for models on a structured reasoning
// channel).
// It buffers partial closing tags across chunk boundaries so a `</thi` split
// across two deltas is not mistaken for prose.
const OPEN = '<think>';
const CLOSE = '</think>';
const LEADING_WS = /^[ \t\r\n]+/;
type State = 'probe' | 'inside' | 'passthrough';
export interface SplitResult {
/** Text classified as reasoning (the inside of a <think> block). */
reasoning: string;
/** Text classified as ordinary content to pass through. */
text: string;
}
/**
* Longest proper suffix of `s` that is a prefix of `marker`. Used to hold back
* the bytes that might be the start of a closing tag split across chunks. Never
* returns the full marker length (that is a complete match, handled separately).
*/
function markerSuffixLen(s: string, marker: string): number {
const max = Math.min(marker.length - 1, s.length);
for (let n = max; n > 0; n--) {
if (marker.startsWith(s.slice(s.length - n))) return n;
}
return 0;
}
/** Stateful, single-stream splitter. Create one per streamed completion. */
export class ThinkSplitter {
private state: State = 'probe';
private buf = '';
push(s: string): SplitResult {
if (this.state === 'passthrough') return { reasoning: '', text: s };
if (this.state === 'inside') return this.scanClose(s);
// probe
this.buf += s;
const trimmed = this.buf.replace(LEADING_WS, '');
if (trimmed.length < OPEN.length) {
// Not enough yet to decide. Hold only if still a viable <think> prefix.
if (OPEN.startsWith(trimmed)) return { reasoning: '', text: '' };
return this.drainPassthrough();
}
if (trimmed.startsWith(OPEN)) {
this.state = 'inside';
this.buf = '';
return this.scanClose(trimmed.slice(OPEN.length));
}
return this.drainPassthrough();
}
/** Resolve any buffered remainder at stream end. */
flush(): SplitResult {
const r = this.buf;
this.buf = '';
if (this.state === 'inside') return { reasoning: r, text: '' };
return { reasoning: '', text: r };
}
private scanClose(s: string): SplitResult {
this.buf += s;
const idx = this.buf.indexOf(CLOSE);
if (idx >= 0) {
const reasoning = this.buf.slice(0, idx);
const text = this.buf.slice(idx + CLOSE.length).replace(LEADING_WS, '');
this.buf = '';
this.state = 'passthrough';
return { reasoning, text };
}
// No full closing tag yet — emit everything except a possible partial tag.
const keep = markerSuffixLen(this.buf, CLOSE);
const reasoning = this.buf.slice(0, this.buf.length - keep);
this.buf = this.buf.slice(this.buf.length - keep);
return { reasoning, text: '' };
}
private drainPassthrough(): SplitResult {
const text = this.buf;
this.buf = '';
this.state = 'passthrough';
return { reasoning: '', text };
}
}