From a32edf423b11948523da58d3cb3a27c19de446e1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 21 Feb 2026 22:45:32 +0000 Subject: [PATCH] refactor(text): share code-region parsing for reasoning tags --- src/shared/text/code-regions.ts | 31 +++++++++++++++++ src/shared/text/reasoning-tags.ts | 33 +------------------ .../reasoning-lane-coordinator.test.ts | 4 +++ src/telegram/reasoning-lane-coordinator.ts | 33 +------------------ 4 files changed, 37 insertions(+), 64 deletions(-) create mode 100644 src/shared/text/code-regions.ts diff --git a/src/shared/text/code-regions.ts b/src/shared/text/code-regions.ts new file mode 100644 index 00000000000..c05328ec70b --- /dev/null +++ b/src/shared/text/code-regions.ts @@ -0,0 +1,31 @@ +export interface CodeRegion { + start: number; + end: number; +} + +export function findCodeRegions(text: string): CodeRegion[] { + const regions: CodeRegion[] = []; + + const fencedRe = /(^|\n)(```|~~~)[^\n]*\n[\s\S]*?(?:\n\2(?:\n|$)|$)/g; + for (const match of text.matchAll(fencedRe)) { + const start = (match.index ?? 0) + match[1].length; + regions.push({ start, end: start + match[0].length - match[1].length }); + } + + const inlineRe = /`+[^`]+`+/g; + for (const match of text.matchAll(inlineRe)) { + const start = match.index ?? 0; + const end = start + match[0].length; + const insideFenced = regions.some((r) => start >= r.start && end <= r.end); + if (!insideFenced) { + regions.push({ start, end }); + } + } + + regions.sort((a, b) => a.start - b.start); + return regions; +} + +export function isInsideCode(pos: number, regions: CodeRegion[]): boolean { + return regions.some((r) => pos >= r.start && pos < r.end); +} diff --git a/src/shared/text/reasoning-tags.ts b/src/shared/text/reasoning-tags.ts index 426d0832201..fcf508b5724 100644 --- a/src/shared/text/reasoning-tags.ts +++ b/src/shared/text/reasoning-tags.ts @@ -1,3 +1,4 @@ +import { findCodeRegions, isInsideCode } from "./code-regions.js"; export type ReasoningTagMode = "strict" | "preserve"; export type ReasoningTagTrim = "none" | "start" | "both"; @@ -5,38 +6,6 @@ const QUICK_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking|final)\b/i; const FINAL_TAG_RE = /<\s*\/?\s*final\b[^<>]*>/gi; const THINKING_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/gi; -interface CodeRegion { - start: number; - end: number; -} - -function findCodeRegions(text: string): CodeRegion[] { - const regions: CodeRegion[] = []; - - const fencedRe = /(^|\n)(```|~~~)[^\n]*\n[\s\S]*?(?:\n\2(?:\n|$)|$)/g; - for (const match of text.matchAll(fencedRe)) { - const start = (match.index ?? 0) + match[1].length; - regions.push({ start, end: start + match[0].length - match[1].length }); - } - - const inlineRe = /`+[^`]+`+/g; - for (const match of text.matchAll(inlineRe)) { - const start = match.index ?? 0; - const end = start + match[0].length; - const insideFenced = regions.some((r) => start >= r.start && end <= r.end); - if (!insideFenced) { - regions.push({ start, end }); - } - } - - regions.sort((a, b) => a.start - b.start); - return regions; -} - -function isInsideCode(pos: number, regions: CodeRegion[]): boolean { - return regions.some((r) => pos >= r.start && pos < r.end); -} - function applyTrim(value: string, mode: ReasoningTagTrim): string { if (mode === "none") { return value; diff --git a/src/telegram/reasoning-lane-coordinator.test.ts b/src/telegram/reasoning-lane-coordinator.test.ts index 2dd3a94647f..795efcf8c49 100644 --- a/src/telegram/reasoning-lane-coordinator.test.ts +++ b/src/telegram/reasoning-lane-coordinator.test.ts @@ -22,4 +22,8 @@ describe("splitTelegramReasoningText", () => { answerText: text, }); }); + + it("does not emit partial reasoning tag prefixes", () => { + expect(splitTelegramReasoningText(" ]*>/gi; -interface CodeRegion { - start: number; - end: number; -} - -function findCodeRegions(text: string): CodeRegion[] { - const regions: CodeRegion[] = []; - - const fencedRe = /(^|\n)(```|~~~)[^\n]*\n[\s\S]*?(?:\n\2(?:\n|$)|$)/g; - for (const match of text.matchAll(fencedRe)) { - const start = (match.index ?? 0) + match[1].length; - regions.push({ start, end: start + match[0].length - match[1].length }); - } - - const inlineRe = /`+[^`]+`+/g; - for (const match of text.matchAll(inlineRe)) { - const start = match.index ?? 0; - const end = start + match[0].length; - const insideFenced = regions.some((r) => start >= r.start && end <= r.end); - if (!insideFenced) { - regions.push({ start, end }); - } - } - - regions.sort((a, b) => a.start - b.start); - return regions; -} - -function isInsideCode(pos: number, regions: CodeRegion[]): boolean { - return regions.some((r) => pos >= r.start && pos < r.end); -} - function extractThinkingFromTaggedStreamOutsideCode(text: string): string { if (!text) { return "";