Skip to content

Commit 68bdd52

Browse files
feat: repetition detection stops stream + resends; multi-answer handling
- Add _createRepetitionState / _checkRepetition / _handleRepetition helpers - All 6 SSE streaming providers (Gemini, Puter, Mistral, Cerebras, Groq, Vercel, OpenAiCompat) now detect when 75-token window repeats in answer text - Repetition between a thinking-end and current position is forgiven - On detection: abort stream, show user-side bubble, send message to model - _separateInlineThinking already handles multiple answer segments correctly (from previous commit: first answer token → everything after is answer)
1 parent 3aea74d commit 68bdd52

1 file changed

Lines changed: 159 additions & 8 deletions

File tree

index.html

Lines changed: 159 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2292,7 +2292,70 @@
22922292
}
22932293

22942294
/* ── GEMINI REST API ─────────────────────────────────────────── */
2295-
/* ── THINKING DISPLAY HELPER ─────────────────────────────────────
2295+
2296+
/* ── REPETITION DETECTOR ──────────────────────────────────────────
2297+
Checks whether the model has repeated a 75-token (~300-char) window
2298+
at least twice in the ANSWER portion of the accumulated text.
2299+
Repetition is only flagged if no thinking block ended BETWEEN the
2300+
two occurrences (a model legitimately restating something after
2301+
reasoning is fine).
2302+
2303+
Returns true when repetition is detected and the stream should be
2304+
aborted. Call once per streaming delta with the current raw answer
2305+
text (NOT the composed thinking+answer string).
2306+
2307+
State is kept in the returned object so callers can reset between
2308+
requests by calling _createRepetitionState(). */
2309+
function _createRepetitionState() {
2310+
return {
2311+
lastThinkingEndIdx: 0, // char index in answer where the last </think> was closed
2312+
checked: false, // true once we have enough text to start checking
2313+
};
2314+
}
2315+
2316+
const REPETITION_TOKEN_WINDOW = 75; // tokens ≈ chars / 4 → 300 chars
2317+
const REPETITION_WINDOW_CHARS = REPETITION_TOKEN_WINDOW * 4;
2318+
2319+
function _checkRepetition(answerText, thinkingEndedSinceLastCheck, state) {
2320+
// Need at least 2× window to find a repeat
2321+
if (answerText.length < REPETITION_WINDOW_CHARS * 2) return false;
2322+
2323+
// If thinking ended since we last checked, reset the "last thinking end" marker
2324+
// so a repetition across a thinking boundary is forgiven.
2325+
if (thinkingEndedSinceLastCheck) {
2326+
state.lastThinkingEndIdx = answerText.length;
2327+
}
2328+
2329+
// Only inspect the portion of the answer produced AFTER the last thinking block ended
2330+
const relevantText = answerText.slice(state.lastThinkingEndIdx);
2331+
if (relevantText.length < REPETITION_WINDOW_CHARS * 2) return false;
2332+
2333+
// Slide a window of REPETITION_WINDOW_CHARS over relevantText and look for
2334+
// a duplicate earlier in the same text.
2335+
const window = relevantText.slice(-REPETITION_WINDOW_CHARS);
2336+
const searchIn = relevantText.slice(0, relevantText.length - REPETITION_WINDOW_CHARS);
2337+
if (searchIn.indexOf(window) !== -1) return true;
2338+
2339+
return false;
2340+
}
2341+
2342+
/* ── REPETITION STOP HELPER ───────────────────────────────────────
2343+
Call when repetition is detected. Aborts the stream and injects a
2344+
user-side "you're repeating yourself" bubble + sends it as the next
2345+
user message to the model, so the model can recover. */
2346+
function _handleRepetition(abortController) {
2347+
if (abortController) {
2348+
try { abortController.abort(); } catch(e) {}
2349+
}
2350+
const repeatMsg = "You're repeating yourself. If necessary, write an EOF token.";
2351+
// Show as a right-side user bubble immediately
2352+
addUserBubble(repeatMsg);
2353+
// Send to model as next user message
2354+
Bridge.sendMessage(repeatMsg);
2355+
window.onGenerationStateChanged(true, false);
2356+
}
2357+
2358+
/* ── THINKING / ANSWER SEPARATOR (INLINE) ─────────────────────── */
22962359
Combines a model's thinking/reasoning text with its final answer into a single
22972360
plain-text string, since the native chat bubble only renders plain text (no
22982361
separate channel for reasoning exists in the WebView<->native bridge). The
@@ -2446,6 +2509,8 @@
24462509
window.__customModelAbortController = new AbortController();
24472510
let acc = '';
24482511
let thinkingAcc = '';
2512+
const _geminiRepState = _createRepetitionState();
2513+
let _geminiThinkingEndedSinceCheck = false;
24492514
try {
24502515
let response = await fetch(url, {
24512516
method: 'POST',
@@ -2507,14 +2572,28 @@
25072572
const json = JSON.parse(data);
25082573
const parts = json.candidates?.[0]?.content?.parts || [];
25092574
let text = '';
2575+
let gotThinkingEnd = false;
25102576
for (const part of parts) {
25112577
if (!part.text) continue;
2512-
if (part.thought) thinkingAcc += part.text;
2513-
else text += part.text;
2578+
if (part.thought) {
2579+
const prevLen = thinkingAcc.length;
2580+
thinkingAcc += part.text;
2581+
} else {
2582+
text += part.text;
2583+
}
25142584
}
2585+
// Track whether a thinking block finished (thought→non-thought transition)
2586+
if (text && thinkingAcc) gotThinkingEnd = true;
2587+
if (gotThinkingEnd) _geminiThinkingEndedSinceCheck = true;
25152588
if (text || thinkingAcc) {
25162589
acc += text;
25172590
Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
2591+
if (_checkRepetition(acc, _geminiThinkingEndedSinceCheck, _geminiRepState)) {
2592+
_geminiThinkingEndedSinceCheck = false;
2593+
_handleRepetition(window.__customModelAbortController);
2594+
return;
2595+
}
2596+
_geminiThinkingEndedSinceCheck = false;
25182597
}
25192598
} catch(e2) { /* ignore bad SSE chunk */ }
25202599
}
@@ -2649,6 +2728,8 @@
26492728
let rawContentAcc = '';
26502729
let structuredThinkingAcc = '';
26512730
let puterFirstAnswerSeen = false;
2731+
const _puterRepState = _createRepetitionState();
2732+
let _puterThinkingEndedSinceCheck = false;
26522733
const reader = response.body.getReader();
26532734
const decoder = new TextDecoder();
26542735
let buf = '';
@@ -2670,6 +2751,7 @@
26702751
const delta = typeof d.content === 'string' ? d.content : _reasoningText(d.content);
26712752
// Once the first answer token has been seen, any further structured
26722753
// reasoning_content is also treated as answer (appended to raw content).
2754+
const hadThinking = !!structuredThinkingAcc;
26732755
if (thinkingDelta) {
26742756
if (puterFirstAnswerSeen) {
26752757
rawContentAcc += thinkingDelta;
@@ -2681,11 +2763,20 @@
26812763
rawContentAcc += delta;
26822764
puterFirstAnswerSeen = true;
26832765
}
2766+
// Thinking ended if we had thinking before and now have answer delta
2767+
if (hadThinking && delta) _puterThinkingEndedSinceCheck = true;
26842768
if (thinkingDelta || delta) {
26852769
const inline = _separateInlineThinking(rawContentAcc);
26862770
Bridge.onCustomModelPartialResponse(
26872771
_composeThinkingAndAnswer(structuredThinkingAcc + inline.thinking, inline.answer)
26882772
);
2773+
const answerSoFar = inline.answer;
2774+
if (_checkRepetition(answerSoFar, _puterThinkingEndedSinceCheck, _puterRepState)) {
2775+
_puterThinkingEndedSinceCheck = false;
2776+
_handleRepetition(window.__customModelAbortController);
2777+
return;
2778+
}
2779+
_puterThinkingEndedSinceCheck = false;
26892780
}
26902781
} catch(e2) { /* ignore bad SSE chunk */ }
26912782
}
@@ -2892,6 +2983,8 @@
28922983
window.__customModelAbortController = new AbortController();
28932984
let acc = '';
28942985
let thinkingAcc = '';
2986+
const _mistralRepState = _createRepetitionState();
2987+
let _mistralThinkingEndedSinceCheck = false;
28952988
try {
28962989
const { response } = await _mistralCoordinatedFetch(
28972990
apiKeys, maxAttempts, minIntervalMs,
@@ -2958,11 +3051,21 @@
29583051
}
29593052
// Once the first answer token has been seen, any further structured
29603053
// thinking deltas are treated as answer content, not reasoning.
3054+
const _mHadThinking = !!thinkingAcc && !acc;
29613055
if (thinkingDelta) {
29623056
if (acc) { acc += thinkingDelta; } else { thinkingAcc += thinkingDelta; }
29633057
}
29643058
if (delta) acc += delta;
2965-
if (thinkingDelta || delta) Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
3059+
if (_mHadThinking && acc) _mistralThinkingEndedSinceCheck = true;
3060+
if (thinkingDelta || delta) {
3061+
Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
3062+
if (_checkRepetition(acc, _mistralThinkingEndedSinceCheck, _mistralRepState)) {
3063+
_mistralThinkingEndedSinceCheck = false;
3064+
_handleRepetition(window.__customModelAbortController);
3065+
return;
3066+
}
3067+
_mistralThinkingEndedSinceCheck = false;
3068+
}
29663069
} catch(e2) { /* ignore bad SSE chunk */ }
29673070
}
29683071
}
@@ -3022,6 +3125,8 @@
30223125
window.__customModelAbortController = new AbortController();
30233126
let acc = '';
30243127
let thinkingAcc = '';
3128+
const _cerebrasRepState = _createRepetitionState();
3129+
let _cerebrasThinkingEndedSinceCheck = false;
30253130
try {
30263131
let response = await fetch('https://api.cerebras.ai/v1/chat/completions', {
30273132
method: 'POST',
@@ -3086,11 +3191,21 @@
30863191
const thinkingDelta = d.reasoning_content || d.reasoning || '';
30873192
const delta = d.content || '';
30883193
// Once the first answer token has been seen, further thinking deltas are answer.
3194+
const _cHadThinking = !!thinkingAcc && !acc;
30893195
if (thinkingDelta) {
30903196
if (acc) { acc += thinkingDelta; } else { thinkingAcc += thinkingDelta; }
30913197
}
30923198
if (delta) acc += delta;
3093-
if (thinkingDelta || delta) Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
3199+
if (_cHadThinking && acc) _cerebrasThinkingEndedSinceCheck = true;
3200+
if (thinkingDelta || delta) {
3201+
Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
3202+
if (_checkRepetition(acc, _cerebrasThinkingEndedSinceCheck, _cerebrasRepState)) {
3203+
_cerebrasThinkingEndedSinceCheck = false;
3204+
_handleRepetition(window.__customModelAbortController);
3205+
return;
3206+
}
3207+
_cerebrasThinkingEndedSinceCheck = false;
3208+
}
30943209
} catch(e2) { /* ignore bad SSE chunk */ }
30953210
}
30963211
}
@@ -3191,6 +3306,8 @@
31913306

31923307
let acc = '';
31933308
let thinkingAcc = '';
3309+
const _groqRepState = _createRepetitionState();
3310+
let _groqThinkingEndedSinceCheck = false;
31943311
const reader = response.body.getReader();
31953312
const decoder = new TextDecoder();
31963313
let buf = '';
@@ -3211,11 +3328,21 @@
32113328
const thinkingDelta = d.reasoning_content || d.reasoning || '';
32123329
const delta = d.content || '';
32133330
// Once the first answer token has been seen, further thinking deltas are answer.
3331+
const _vHadThinking = !!thinkingAcc && !acc;
32143332
if (thinkingDelta) {
32153333
if (acc) { acc += thinkingDelta; } else { thinkingAcc += thinkingDelta; }
32163334
}
32173335
if (delta) acc += delta;
3218-
if (thinkingDelta || delta) Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
3336+
if (_vHadThinking && acc) _vercelThinkingEndedSinceCheck = true;
3337+
if (thinkingDelta || delta) {
3338+
Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
3339+
if (_checkRepetition(acc, _vercelThinkingEndedSinceCheck, _vercelRepState)) {
3340+
_vercelThinkingEndedSinceCheck = false;
3341+
_handleRepetition(window.__customModelAbortController);
3342+
return;
3343+
}
3344+
_vercelThinkingEndedSinceCheck = false;
3345+
}
32193346
} catch(e2) { /* ignore bad SSE chunk */ }
32203347
}
32213348
}
@@ -3288,6 +3415,8 @@
32883415
window.__customModelAbortController = new AbortController();
32893416
let acc = '';
32903417
let thinkingAcc = '';
3418+
const _vercelRepState = _createRepetitionState();
3419+
let _vercelThinkingEndedSinceCheck = false;
32913420
try {
32923421
let response = await fetch(vercelChatEndpoint, {
32933422
method: 'POST',
@@ -3351,11 +3480,21 @@
33513480
const thinkingDelta = d.reasoning_content || d.reasoning || '';
33523481
const delta = d.content || '';
33533482
// Once the first answer token has been seen, further thinking deltas are answer.
3483+
const _gHadThinking = !!thinkingAcc && !acc;
33543484
if (thinkingDelta) {
33553485
if (acc) { acc += thinkingDelta; } else { thinkingAcc += thinkingDelta; }
33563486
}
33573487
if (delta) acc += delta;
3358-
if (thinkingDelta || delta) Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
3488+
if (_gHadThinking && acc) _groqThinkingEndedSinceCheck = true;
3489+
if (thinkingDelta || delta) {
3490+
Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
3491+
if (_checkRepetition(acc, _groqThinkingEndedSinceCheck, _groqRepState)) {
3492+
_groqThinkingEndedSinceCheck = false;
3493+
_handleRepetition(window.__customModelAbortController);
3494+
return;
3495+
}
3496+
_groqThinkingEndedSinceCheck = false;
3497+
}
33593498
} catch(e2) { /* ignore bad SSE chunk */ }
33603499
}
33613500
}
@@ -3445,6 +3584,8 @@
34453584
window.__customModelAbortController = new AbortController();
34463585
let acc = '';
34473586
let thinkingAcc = '';
3587+
const _oaicRepState = _createRepetitionState();
3588+
let _oaicThinkingEndedSinceCheck = false;
34483589
try {
34493590
let response = await fetch(endpoint, {
34503591
method: 'POST',
@@ -3517,11 +3658,21 @@
35173658
const thinkingDelta = d.reasoning_content || d.reasoning || '';
35183659
const delta = d.content || '';
35193660
// Once the first answer token has been seen, further thinking deltas are answer.
3661+
const _oHadThinking = !!thinkingAcc && !acc;
35203662
if (thinkingDelta) {
35213663
if (acc) { acc += thinkingDelta; } else { thinkingAcc += thinkingDelta; }
35223664
}
35233665
if (delta) acc += delta;
3524-
if (thinkingDelta || delta) Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
3666+
if (_oHadThinking && acc) _oaicThinkingEndedSinceCheck = true;
3667+
if (thinkingDelta || delta) {
3668+
Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
3669+
if (_checkRepetition(acc, _oaicThinkingEndedSinceCheck, _oaicRepState)) {
3670+
_oaicThinkingEndedSinceCheck = false;
3671+
_handleRepetition(window.__customModelAbortController);
3672+
return;
3673+
}
3674+
_oaicThinkingEndedSinceCheck = false;
3675+
}
35253676
} catch(e2) { /* ignore */ }
35263677
}
35273678
}

0 commit comments

Comments
 (0)