138 lines
4.4 KiB
TypeScript
138 lines
4.4 KiB
TypeScript
// Grok markup cleanup (pure). Extracted verbatim from grok-web.ts.
|
|
import type { GrokStreamResponse } from "./types.ts";
|
|
|
|
// ─── Grok markup cleanup ────────────────────────────────────────────────────
|
|
|
|
export const BLOCKED_GROK_MARKUP = [
|
|
{ start: "<xai:tool_usage_card", end: "</xai:tool_usage_card>" },
|
|
] as const;
|
|
|
|
export const PARTIAL_GROK_MARKER_KEEP = 32;
|
|
|
|
export function stripLooseGrokMarkup(text: string): string {
|
|
return text
|
|
.replace(/<\/?xai:[^>]*>/g, "")
|
|
.replace(/<\/?grok:[^>]*>/g, "")
|
|
.replace(/<\/?argument\b[^>]*>/g, "")
|
|
.replace(/<!\[CDATA\[/g, "")
|
|
.replace(/\]\]>/g, "");
|
|
}
|
|
|
|
export class GrokMarkupFilter {
|
|
private buffer = "";
|
|
private suppressedUntil: string | null = null;
|
|
|
|
feed(text: string): string {
|
|
if (!text) return "";
|
|
this.buffer += text;
|
|
return this.drain(false);
|
|
}
|
|
|
|
flush(): string {
|
|
const out = this.drain(true);
|
|
this.buffer = "";
|
|
this.suppressedUntil = null;
|
|
return out;
|
|
}
|
|
|
|
private drain(flush: boolean): string {
|
|
let out = "";
|
|
|
|
while (this.buffer) {
|
|
if (this.suppressedUntil) {
|
|
const endIdx = this.buffer.indexOf(this.suppressedUntil);
|
|
if (endIdx < 0) {
|
|
this.buffer = this.buffer.slice(this.longestEndPrefixStart(this.suppressedUntil));
|
|
return out;
|
|
}
|
|
this.buffer = this.buffer.slice(endIdx + this.suppressedUntil.length);
|
|
this.suppressedUntil = null;
|
|
continue;
|
|
}
|
|
|
|
let nextStart = -1;
|
|
let nextEnd = "";
|
|
for (const marker of BLOCKED_GROK_MARKUP) {
|
|
const idx = this.buffer.indexOf(marker.start);
|
|
if (idx >= 0 && (nextStart < 0 || idx < nextStart)) {
|
|
nextStart = idx;
|
|
nextEnd = marker.end;
|
|
}
|
|
}
|
|
|
|
if (nextStart < 0) {
|
|
if (!flush) {
|
|
const lastLt = this.buffer.lastIndexOf("<");
|
|
if (lastLt >= 0 && this.buffer.length - lastLt <= PARTIAL_GROK_MARKER_KEEP) {
|
|
out += stripLooseGrokMarkup(this.buffer.slice(0, lastLt));
|
|
this.buffer = this.buffer.slice(lastLt);
|
|
return out;
|
|
}
|
|
}
|
|
out += stripLooseGrokMarkup(this.buffer);
|
|
this.buffer = "";
|
|
return out;
|
|
}
|
|
|
|
out += stripLooseGrokMarkup(this.buffer.slice(0, nextStart));
|
|
this.buffer = this.buffer.slice(nextStart);
|
|
const endIdx = this.buffer.indexOf(nextEnd);
|
|
const openTagEndIdx = this.buffer.indexOf(">");
|
|
if (openTagEndIdx >= 0 && /\/\s*>$/.test(this.buffer.slice(0, openTagEndIdx + 1))) {
|
|
this.buffer = this.buffer.slice(openTagEndIdx + 1);
|
|
continue;
|
|
}
|
|
if (endIdx < 0) {
|
|
this.suppressedUntil = nextEnd;
|
|
this.buffer = this.buffer.slice(this.longestEndPrefixStart(nextEnd));
|
|
return out;
|
|
}
|
|
this.buffer = this.buffer.slice(endIdx + nextEnd.length);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
private longestEndPrefixStart(end: string): number {
|
|
const max = Math.min(this.buffer.length, end.length - 1);
|
|
for (let len = max; len > 0; len--) {
|
|
if (this.buffer.slice(-len) === end.slice(0, len)) return this.buffer.length - len;
|
|
}
|
|
return this.buffer.length;
|
|
}
|
|
}
|
|
|
|
export function cleanGrokText(text: string): string {
|
|
const filter = new GrokMarkupFilter();
|
|
return filter.feed(text) + filter.flush();
|
|
}
|
|
|
|
export function cleanGrokContentText(text: string): string {
|
|
return cleanGrokText(text);
|
|
}
|
|
|
|
export function cleanGrokThinkingText(resp: GrokStreamResponse): string {
|
|
const text = resp.token || "";
|
|
const cleaned = cleanGrokText(text);
|
|
const trimmed = cleaned.trim();
|
|
if (!trimmed) return "";
|
|
const isGenericOpeningHeader =
|
|
resp.messageTag === "header" &&
|
|
resp.messageStepId === 0 &&
|
|
/^(?:\.{3}|thinking(?: about your request)?)$/i.test(trimmed);
|
|
if (isGenericOpeningHeader) return "";
|
|
if (resp.messageTag === "header") return `${trimmed}\n`;
|
|
if (resp.messageTag === "summary") return `${trimmed}\n`;
|
|
return cleaned;
|
|
}
|
|
|
|
export function extractStructuredReasoning(value: object | undefined): string {
|
|
if (!value) return "";
|
|
const record = value as Record<string, unknown>;
|
|
for (const key of ["reasoning", "reasoningContent", "reasoning_content", "thinking", "thought"]) {
|
|
const candidate = record[key];
|
|
if (typeof candidate === "string" && candidate.trim()) return cleanGrokText(candidate);
|
|
}
|
|
return "";
|
|
}
|