| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | export interface ThinkDelta { |
| | type: "reasoning" | "content"; |
| | textDelta: string; |
| | } |
| |
|
| | export class ThinkStreamParser { |
| | |
| | reasoning = ""; |
| | |
| | content = ""; |
| |
|
| | |
| | private _inThink = false; |
| | |
| | private _buf = ""; |
| |
|
| | private static readonly OPEN_TAG = "<think>"; |
| | private static readonly CLOSE_TAG = "</think>"; |
| |
|
| | reset(): void { |
| | this.reasoning = ""; |
| | this.content = ""; |
| | this._inThink = false; |
| | this._buf = ""; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | push(text: string): ThinkDelta[] { |
| | const deltas: ThinkDelta[] = []; |
| | this._buf += text; |
| |
|
| | while (this._buf.length > 0) { |
| | if (this._inThink) { |
| | const closeIdx = this._buf.indexOf(ThinkStreamParser.CLOSE_TAG); |
| | if (closeIdx !== -1) { |
| | const before = this._buf.slice(0, closeIdx); |
| | if (before) { |
| | this.reasoning += before; |
| | deltas.push({ type: "reasoning", textDelta: before }); |
| | } |
| | this._buf = this._buf.slice( |
| | closeIdx + ThinkStreamParser.CLOSE_TAG.length, |
| | ); |
| | this._inThink = false; |
| | continue; |
| | } |
| |
|
| | |
| | const safeLen = this._safeFlushLength( |
| | this._buf, |
| | ThinkStreamParser.CLOSE_TAG, |
| | ); |
| | if (safeLen > 0) { |
| | const chunk = this._buf.slice(0, safeLen); |
| | this.reasoning += chunk; |
| | deltas.push({ type: "reasoning", textDelta: chunk }); |
| | this._buf = this._buf.slice(safeLen); |
| | } |
| | break; |
| | } else { |
| | const openIdx = this._buf.indexOf(ThinkStreamParser.OPEN_TAG); |
| | if (openIdx !== -1) { |
| | const before = this._buf.slice(0, openIdx); |
| | if (before) { |
| | this.content += before; |
| | deltas.push({ type: "content", textDelta: before }); |
| | } |
| | this._buf = this._buf.slice( |
| | openIdx + ThinkStreamParser.OPEN_TAG.length, |
| | ); |
| | this._inThink = true; |
| | continue; |
| | } |
| |
|
| | |
| | const safeLen = this._safeFlushLength( |
| | this._buf, |
| | ThinkStreamParser.OPEN_TAG, |
| | ); |
| | if (safeLen > 0) { |
| | const chunk = this._buf.slice(0, safeLen); |
| | this.content += chunk; |
| | deltas.push({ type: "content", textDelta: chunk }); |
| | this._buf = this._buf.slice(safeLen); |
| | } |
| | break; |
| | } |
| | } |
| |
|
| | return deltas; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | flush(): ThinkDelta[] { |
| | if (!this._buf) return []; |
| | const deltas: ThinkDelta[] = []; |
| | if (this._inThink) { |
| | this.reasoning += this._buf; |
| | deltas.push({ type: "reasoning", textDelta: this._buf }); |
| | } else { |
| | this.content += this._buf; |
| | deltas.push({ type: "content", textDelta: this._buf }); |
| | } |
| | this._buf = ""; |
| | return deltas; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | private _safeFlushLength(buf: string, tag: string): number { |
| | |
| | for (let overlap = Math.min(buf.length, tag.length - 1); overlap > 0; overlap--) { |
| | if (buf.endsWith(tag.slice(0, overlap))) { |
| | return buf.length - overlap; |
| | } |
| | } |
| | return buf.length; |
| | } |
| | } |
| |
|