|
1943 | 1943 | } |
1944 | 1944 |
|
1945 | 1945 | /* ── 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 | + |
1946 | 1959 | async function _callGemini(payload) { |
1947 | 1960 | const keys = Bridge.getAllApiKeys('GOOGLE'); |
1948 | 1961 | if (!keys.length) { |
|
1985 | 1998 | if (payload.temperature !== undefined && payload.temperature > 0) genConfig.temperature = payload.temperature; |
1986 | 1999 | if (payload.topP !== undefined && payload.topP > 0) genConfig.topP = payload.topP; |
1987 | 2000 | 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 }; |
1988 | 2004 |
|
1989 | 2005 | const requestBody = { contents }; |
1990 | 2006 | if (systemParts.length) requestBody.systemInstruction = { parts: systemParts }; |
1991 | 2007 | if (Object.keys(genConfig).length) requestBody.generationConfig = genConfig; |
1992 | 2008 |
|
1993 | 2009 | window.__customModelAbortController = new AbortController(); |
1994 | 2010 | let acc = ''; |
| 2011 | + let thinkingAcc = ''; |
1995 | 2012 | try { |
1996 | 2013 | const response = await fetch(url, { |
1997 | 2014 | method: 'POST', |
|
2030 | 2047 | const parts = json.candidates?.[0]?.content?.parts || []; |
2031 | 2048 | let text = ''; |
2032 | 2049 | 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)); |
2034 | 2057 | } |
2035 | | - if (text) { acc += text; Bridge.onCustomModelPartialResponse(acc); } |
2036 | 2058 | } catch(e2) { /* ignore bad SSE chunk */ } |
2037 | 2059 | } |
2038 | 2060 | } |
2039 | | - Bridge.onCustomModelFinalResponse(acc); |
| 2061 | + const finalText = _composeThinkingAndAnswer(thinkingAcc, acc); |
| 2062 | + Bridge.onCustomModelFinalResponse(finalText); |
2040 | 2063 | await _executeCommandsFromResponse(acc); |
2041 | 2064 | } catch(e) { |
2042 | 2065 | 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]'); |
2044 | 2068 | } else { |
2045 | 2069 | throw e; |
2046 | 2070 | } |
|
2113 | 2137 | } |
2114 | 2138 |
|
2115 | 2139 | let acc = ''; |
| 2140 | + let thinkingAcc = ''; |
2116 | 2141 | try { |
2117 | 2142 | 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 || ''; |
2119 | 2148 | } catch(e2) { |
2120 | 2149 | throw new Error('Puter: failed to parse response JSON'); |
2121 | 2150 | } |
2122 | | - if (!acc) throw new Error('No response from model'); |
| 2151 | + if (!acc && !thinkingAcc) throw new Error('No response from model'); |
2123 | 2152 |
|
2124 | | - Bridge.onCustomModelPartialResponse(acc); |
2125 | | - Bridge.onCustomModelFinalResponse(acc); |
| 2153 | + const composed = _composeThinkingAndAnswer(thinkingAcc, acc); |
| 2154 | + Bridge.onCustomModelPartialResponse(composed); |
| 2155 | + Bridge.onCustomModelFinalResponse(composed); |
2126 | 2156 | await _executeCommandsFromResponse(acc); |
2127 | 2157 | } catch(e) { |
2128 | 2158 | if (e && e.name === 'AbortError') { |
|
2308 | 2338 |
|
2309 | 2339 | window.__customModelAbortController = new AbortController(); |
2310 | 2340 | let acc = ''; |
| 2341 | + let thinkingAcc = ''; |
2311 | 2342 | try { |
2312 | 2343 | const { response } = await _mistralCoordinatedFetch( |
2313 | 2344 | apiKeys, maxAttempts, minIntervalMs, |
|
2342 | 2373 | if (!data || data === '[DONE]') continue; |
2343 | 2374 | try { |
2344 | 2375 | 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)); |
2347 | 2384 | } catch(e2) { /* ignore bad SSE chunk */ } |
2348 | 2385 | } |
2349 | 2386 | } |
2350 | | - Bridge.onCustomModelFinalResponse(acc); |
| 2387 | + Bridge.onCustomModelFinalResponse(_composeThinkingAndAnswer(thinkingAcc, acc)); |
2351 | 2388 | await _executeCommandsFromResponse(acc); |
2352 | 2389 | } catch(e) { |
2353 | 2390 | 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]'); |
2355 | 2393 | } else { |
2356 | 2394 | throw e; |
2357 | 2395 | } |
|
2401 | 2439 |
|
2402 | 2440 | window.__customModelAbortController = new AbortController(); |
2403 | 2441 | let acc = ''; |
| 2442 | + let thinkingAcc = ''; |
2404 | 2443 | try { |
2405 | 2444 | let response = await fetch('https://api.cerebras.ai/v1/chat/completions', { |
2406 | 2445 | method: 'POST', |
|
2449 | 2488 | if (!data || data === '[DONE]') continue; |
2450 | 2489 | try { |
2451 | 2490 | 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)); |
2454 | 2499 | } catch(e2) { /* ignore bad SSE chunk */ } |
2455 | 2500 | } |
2456 | 2501 | } |
2457 | | - Bridge.onCustomModelFinalResponse(acc); |
| 2502 | + Bridge.onCustomModelFinalResponse(_composeThinkingAndAnswer(thinkingAcc, acc)); |
2458 | 2503 | await _executeCommandsFromResponse(acc); |
2459 | 2504 | } catch(e) { |
2460 | 2505 | 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]'); |
2462 | 2508 | } else { |
2463 | 2509 | throw e; |
2464 | 2510 | } |
|
2515 | 2561 | } |
2516 | 2562 |
|
2517 | 2563 | let acc = ''; |
| 2564 | + let thinkingAcc = ''; |
2518 | 2565 | try { |
2519 | 2566 | 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 || ''; |
2521 | 2572 | } catch(e2) { |
2522 | 2573 | throw new Error('Groq: failed to parse response JSON'); |
2523 | 2574 | } |
2524 | | - if (!acc) throw new Error('No response from model'); |
| 2575 | + if (!acc && !thinkingAcc) throw new Error('No response from model'); |
2525 | 2576 |
|
2526 | | - Bridge.onCustomModelPartialResponse(acc); |
2527 | | - Bridge.onCustomModelFinalResponse(acc); |
| 2577 | + const composed = _composeThinkingAndAnswer(thinkingAcc, acc); |
| 2578 | + Bridge.onCustomModelPartialResponse(composed); |
| 2579 | + Bridge.onCustomModelFinalResponse(composed); |
2528 | 2580 | await _executeCommandsFromResponse(acc); |
2529 | 2581 | } catch(e) { |
2530 | 2582 | if (e && e.name === 'AbortError') { |
|
2568 | 2620 |
|
2569 | 2621 | window.__customModelAbortController = new AbortController(); |
2570 | 2622 | let acc = ''; |
| 2623 | + let thinkingAcc = ''; |
2571 | 2624 | try { |
2572 | 2625 | const response = await fetch('https://v0-screen-operator-clon-pi.vercel.app/api/chat', { |
2573 | 2626 | method: 'POST', |
|
2598 | 2651 | if (!data || data === '[DONE]') continue; |
2599 | 2652 | try { |
2600 | 2653 | 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)); |
2603 | 2662 | } catch(e2) { /* ignore bad SSE chunk */ } |
2604 | 2663 | } |
2605 | 2664 | } |
2606 | | - Bridge.onCustomModelFinalResponse(acc); |
| 2665 | + Bridge.onCustomModelFinalResponse(_composeThinkingAndAnswer(thinkingAcc, acc)); |
2607 | 2666 | await _executeCommandsFromResponse(acc); |
2608 | 2667 | } catch(e) { |
2609 | 2668 | 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]'); |
2611 | 2671 | } else { |
2612 | 2672 | throw e; |
2613 | 2673 | } |
|
2670 | 2730 |
|
2671 | 2731 | window.__customModelAbortController = new AbortController(); |
2672 | 2732 | let acc = ''; |
| 2733 | + let thinkingAcc = ''; |
2673 | 2734 | try { |
2674 | 2735 | const response = await fetch(endpoint, { |
2675 | 2736 | method: 'POST', |
|
2703 | 2764 | if (!data || data === '[DONE]') continue; |
2704 | 2765 | try { |
2705 | 2766 | 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)); |
2708 | 2775 | } catch(e2) { /* ignore */ } |
2709 | 2776 | } |
2710 | 2777 | } |
2711 | 2778 | } else { |
2712 | 2779 | 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 || ''; |
2714 | 2783 | } |
2715 | 2784 |
|
2716 | | - Bridge.onCustomModelFinalResponse(acc); |
| 2785 | + const finalText = _composeThinkingAndAnswer(thinkingAcc, acc); |
| 2786 | + Bridge.onCustomModelFinalResponse(finalText); |
2717 | 2787 | await _executeCommandsFromResponse(acc); |
2718 | 2788 | } catch(e) { |
2719 | 2789 | 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]'); |
2721 | 2792 | } else { |
2722 | 2793 | throw e; |
2723 | 2794 | } |
|
0 commit comments