Skip to content

Commit 235929d

Browse files
feat: show thinking/reasoning for all providers (Gemini, Mistral, Cerebras, Groq, Vercel, Puter, custom OpenAI-compat)
1 parent 2f6a97a commit 235929d

1 file changed

Lines changed: 100 additions & 29 deletions

File tree

index.html

Lines changed: 100 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1943,6 +1943,19 @@
19431943
}
19441944

19451945
/* ── GEMINI REST API ─────────────────────────────────────────── */
1946+
/* ── THINKING DISPLAY HELPER ─────────────────────────────────────
1947+
Combines a model's thinking/reasoning text with its final answer into a single
1948+
plain-text string, since the native chat bubble only renders plain text (no
1949+
separate channel for reasoning exists in the WebView<->native bridge). The
1950+
thinking block is clearly labeled and separated from the answer so the user
1951+
can tell them apart. Used by every provider that can expose reasoning text. */
1952+
function _composeThinkingAndAnswer(thinking, answer) {
1953+
const t = (thinking || '').trim();
1954+
const a = answer || '';
1955+
if (!t) return a;
1956+
return '🧠 Thinking:\n' + t + '\n\n💬 Antwort:\n' + a;
1957+
}
1958+
19461959
async function _callGemini(payload) {
19471960
const keys = Bridge.getAllApiKeys('GOOGLE');
19481961
if (!keys.length) {
@@ -1985,13 +1998,17 @@
19851998
if (payload.temperature !== undefined && payload.temperature > 0) genConfig.temperature = payload.temperature;
19861999
if (payload.topP !== undefined && payload.topP > 0) genConfig.topP = payload.topP;
19872000
if (payload.supportsTopK && payload.topK !== undefined && payload.topK > 0) genConfig.topK = payload.topK;
2001+
// Ask Gemini to include its thought summaries in the stream (supported by the
2002+
// 2.5-series "thinking" models; ignored harmlessly by models that don't support it).
2003+
genConfig.thinkingConfig = { includeThoughts: true };
19882004

19892005
const requestBody = { contents };
19902006
if (systemParts.length) requestBody.systemInstruction = { parts: systemParts };
19912007
if (Object.keys(genConfig).length) requestBody.generationConfig = genConfig;
19922008

19932009
window.__customModelAbortController = new AbortController();
19942010
let acc = '';
2011+
let thinkingAcc = '';
19952012
try {
19962013
const response = await fetch(url, {
19972014
method: 'POST',
@@ -2030,17 +2047,24 @@
20302047
const parts = json.candidates?.[0]?.content?.parts || [];
20312048
let text = '';
20322049
for (const part of parts) {
2033-
if (!part.thought && part.text) text += part.text;
2050+
if (!part.text) continue;
2051+
if (part.thought) thinkingAcc += part.text;
2052+
else text += part.text;
2053+
}
2054+
if (text || thinkingAcc) {
2055+
acc += text;
2056+
Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
20342057
}
2035-
if (text) { acc += text; Bridge.onCustomModelPartialResponse(acc); }
20362058
} catch(e2) { /* ignore bad SSE chunk */ }
20372059
}
20382060
}
2039-
Bridge.onCustomModelFinalResponse(acc);
2061+
const finalText = _composeThinkingAndAnswer(thinkingAcc, acc);
2062+
Bridge.onCustomModelFinalResponse(finalText);
20402063
await _executeCommandsFromResponse(acc);
20412064
} catch(e) {
20422065
if (e && e.name === 'AbortError') {
2043-
Bridge.onCustomModelFinalResponse(acc + (acc ? '\n\n' : '') + '[stopped by user]');
2066+
const stoppedText = _composeThinkingAndAnswer(thinkingAcc, acc);
2067+
Bridge.onCustomModelFinalResponse(stoppedText + (stoppedText ? '\n\n' : '') + '[stopped by user]');
20442068
} else {
20452069
throw e;
20462070
}
@@ -2113,16 +2137,22 @@
21132137
}
21142138

21152139
let acc = '';
2140+
let thinkingAcc = '';
21162141
try {
21172142
const json = JSON.parse(responseBodyString);
2118-
acc = json.choices?.[0]?.message?.content || '';
2143+
const msg = json.choices?.[0]?.message || {};
2144+
acc = msg.content || '';
2145+
// Different OpenAI-compatible reasoning models expose their thinking under
2146+
// different keys - check the common ones.
2147+
thinkingAcc = msg.reasoning_content || msg.reasoning || '';
21192148
} catch(e2) {
21202149
throw new Error('Puter: failed to parse response JSON');
21212150
}
2122-
if (!acc) throw new Error('No response from model');
2151+
if (!acc && !thinkingAcc) throw new Error('No response from model');
21232152

2124-
Bridge.onCustomModelPartialResponse(acc);
2125-
Bridge.onCustomModelFinalResponse(acc);
2153+
const composed = _composeThinkingAndAnswer(thinkingAcc, acc);
2154+
Bridge.onCustomModelPartialResponse(composed);
2155+
Bridge.onCustomModelFinalResponse(composed);
21262156
await _executeCommandsFromResponse(acc);
21272157
} catch(e) {
21282158
if (e && e.name === 'AbortError') {
@@ -2308,6 +2338,7 @@
23082338

23092339
window.__customModelAbortController = new AbortController();
23102340
let acc = '';
2341+
let thinkingAcc = '';
23112342
try {
23122343
const { response } = await _mistralCoordinatedFetch(
23132344
apiKeys, maxAttempts, minIntervalMs,
@@ -2342,16 +2373,23 @@
23422373
if (!data || data === '[DONE]') continue;
23432374
try {
23442375
const json = JSON.parse(data);
2345-
const delta = json.choices?.[0]?.delta?.content || '';
2346-
if (delta) { acc += delta; Bridge.onCustomModelPartialResponse(acc); }
2376+
const d = json.choices?.[0]?.delta || {};
2377+
// Magistral / reasoning-capable Mistral models stream their thinking
2378+
// under delta.reasoning_content (or delta.reasoning) alongside delta.content.
2379+
const thinkingDelta = d.reasoning_content || d.reasoning || '';
2380+
const delta = d.content || '';
2381+
if (thinkingDelta) thinkingAcc += thinkingDelta;
2382+
if (delta) acc += delta;
2383+
if (thinkingDelta || delta) Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
23472384
} catch(e2) { /* ignore bad SSE chunk */ }
23482385
}
23492386
}
2350-
Bridge.onCustomModelFinalResponse(acc);
2387+
Bridge.onCustomModelFinalResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
23512388
await _executeCommandsFromResponse(acc);
23522389
} catch(e) {
23532390
if (e && e.name === 'AbortError') {
2354-
Bridge.onCustomModelFinalResponse(acc + (acc ? '\n\n' : '') + '[stopped by user]');
2391+
const stoppedText = _composeThinkingAndAnswer(thinkingAcc, acc);
2392+
Bridge.onCustomModelFinalResponse(stoppedText + (stoppedText ? '\n\n' : '') + '[stopped by user]');
23552393
} else {
23562394
throw e;
23572395
}
@@ -2401,6 +2439,7 @@
24012439

24022440
window.__customModelAbortController = new AbortController();
24032441
let acc = '';
2442+
let thinkingAcc = '';
24042443
try {
24052444
let response = await fetch('https://api.cerebras.ai/v1/chat/completions', {
24062445
method: 'POST',
@@ -2449,16 +2488,23 @@
24492488
if (!data || data === '[DONE]') continue;
24502489
try {
24512490
const json = JSON.parse(data);
2452-
const delta = json.choices?.[0]?.delta?.content || '';
2453-
if (delta) { acc += delta; Bridge.onCustomModelPartialResponse(acc); }
2491+
const d = json.choices?.[0]?.delta || {};
2492+
// GPT-OSS (Cerebras) is a reasoning model and streams its thinking under
2493+
// delta.reasoning_content (or delta.reasoning) alongside delta.content.
2494+
const thinkingDelta = d.reasoning_content || d.reasoning || '';
2495+
const delta = d.content || '';
2496+
if (thinkingDelta) thinkingAcc += thinkingDelta;
2497+
if (delta) acc += delta;
2498+
if (thinkingDelta || delta) Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
24542499
} catch(e2) { /* ignore bad SSE chunk */ }
24552500
}
24562501
}
2457-
Bridge.onCustomModelFinalResponse(acc);
2502+
Bridge.onCustomModelFinalResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
24582503
await _executeCommandsFromResponse(acc);
24592504
} catch(e) {
24602505
if (e && e.name === 'AbortError') {
2461-
Bridge.onCustomModelFinalResponse(acc + (acc ? '\n\n' : '') + '[stopped by user]');
2506+
const stoppedText = _composeThinkingAndAnswer(thinkingAcc, acc);
2507+
Bridge.onCustomModelFinalResponse(stoppedText + (stoppedText ? '\n\n' : '') + '[stopped by user]');
24622508
} else {
24632509
throw e;
24642510
}
@@ -2515,16 +2561,22 @@
25152561
}
25162562

25172563
let acc = '';
2564+
let thinkingAcc = '';
25182565
try {
25192566
const json = JSON.parse(responseBodyString);
2520-
acc = json.choices?.[0]?.message?.content || '';
2567+
const msg = json.choices?.[0]?.message || {};
2568+
acc = msg.content || '';
2569+
// Groq's reasoning models (e.g. deepseek-r1, qwen3) expose thinking under
2570+
// reasoning_content or reasoning.
2571+
thinkingAcc = msg.reasoning_content || msg.reasoning || '';
25212572
} catch(e2) {
25222573
throw new Error('Groq: failed to parse response JSON');
25232574
}
2524-
if (!acc) throw new Error('No response from model');
2575+
if (!acc && !thinkingAcc) throw new Error('No response from model');
25252576

2526-
Bridge.onCustomModelPartialResponse(acc);
2527-
Bridge.onCustomModelFinalResponse(acc);
2577+
const composed = _composeThinkingAndAnswer(thinkingAcc, acc);
2578+
Bridge.onCustomModelPartialResponse(composed);
2579+
Bridge.onCustomModelFinalResponse(composed);
25282580
await _executeCommandsFromResponse(acc);
25292581
} catch(e) {
25302582
if (e && e.name === 'AbortError') {
@@ -2568,6 +2620,7 @@
25682620

25692621
window.__customModelAbortController = new AbortController();
25702622
let acc = '';
2623+
let thinkingAcc = '';
25712624
try {
25722625
const response = await fetch('https://v0-screen-operator-clon-pi.vercel.app/api/chat', {
25732626
method: 'POST',
@@ -2598,16 +2651,23 @@
25982651
if (!data || data === '[DONE]') continue;
25992652
try {
26002653
const json = JSON.parse(data);
2601-
const delta = json.choices?.[0]?.delta?.content || '';
2602-
if (delta) { acc += delta; Bridge.onCustomModelPartialResponse(acc); }
2654+
const d = json.choices?.[0]?.delta || {};
2655+
// Some GPT reasoning models proxied through this endpoint expose thinking
2656+
// under delta.reasoning_content (or delta.reasoning) alongside delta.content.
2657+
const thinkingDelta = d.reasoning_content || d.reasoning || '';
2658+
const delta = d.content || '';
2659+
if (thinkingDelta) thinkingAcc += thinkingDelta;
2660+
if (delta) acc += delta;
2661+
if (thinkingDelta || delta) Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
26032662
} catch(e2) { /* ignore bad SSE chunk */ }
26042663
}
26052664
}
2606-
Bridge.onCustomModelFinalResponse(acc);
2665+
Bridge.onCustomModelFinalResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
26072666
await _executeCommandsFromResponse(acc);
26082667
} catch(e) {
26092668
if (e && e.name === 'AbortError') {
2610-
Bridge.onCustomModelFinalResponse(acc + (acc ? '\n\n' : '') + '[stopped by user]');
2669+
const stoppedText = _composeThinkingAndAnswer(thinkingAcc, acc);
2670+
Bridge.onCustomModelFinalResponse(stoppedText + (stoppedText ? '\n\n' : '') + '[stopped by user]');
26112671
} else {
26122672
throw e;
26132673
}
@@ -2670,6 +2730,7 @@
26702730

26712731
window.__customModelAbortController = new AbortController();
26722732
let acc = '';
2733+
let thinkingAcc = '';
26732734
try {
26742735
const response = await fetch(endpoint, {
26752736
method: 'POST',
@@ -2703,21 +2764,31 @@
27032764
if (!data || data === '[DONE]') continue;
27042765
try {
27052766
const json = JSON.parse(data);
2706-
const delta = json.choices?.[0]?.delta?.content || '';
2707-
if (delta) { acc += delta; Bridge.onCustomModelPartialResponse(acc); }
2767+
const d = json.choices?.[0]?.delta || {};
2768+
// Reasoning-capable OpenAI-compatible models (custom endpoints included) commonly
2769+
// expose thinking under delta.reasoning_content or delta.reasoning.
2770+
const thinkingDelta = d.reasoning_content || d.reasoning || '';
2771+
const delta = d.content || '';
2772+
if (thinkingDelta) thinkingAcc += thinkingDelta;
2773+
if (delta) acc += delta;
2774+
if (thinkingDelta || delta) Bridge.onCustomModelPartialResponse(_composeThinkingAndAnswer(thinkingAcc, acc));
27082775
} catch(e2) { /* ignore */ }
27092776
}
27102777
}
27112778
} else {
27122779
const json = await response.json();
2713-
acc = json.choices?.[0]?.message?.content || '';
2780+
const msg = json.choices?.[0]?.message || {};
2781+
acc = msg.content || '';
2782+
thinkingAcc = msg.reasoning_content || msg.reasoning || '';
27142783
}
27152784

2716-
Bridge.onCustomModelFinalResponse(acc);
2785+
const finalText = _composeThinkingAndAnswer(thinkingAcc, acc);
2786+
Bridge.onCustomModelFinalResponse(finalText);
27172787
await _executeCommandsFromResponse(acc);
27182788
} catch(e) {
27192789
if (e && e.name === 'AbortError') {
2720-
Bridge.onCustomModelFinalResponse(acc + (acc ? '\n\n' : '') + '[stopped by user]');
2790+
const stoppedText = _composeThinkingAndAnswer(thinkingAcc, acc);
2791+
Bridge.onCustomModelFinalResponse(stoppedText + (stoppedText ? '\n\n' : '') + '[stopped by user]');
27212792
} else {
27222793
throw e;
27232794
}

0 commit comments

Comments
 (0)