diff options
Diffstat (limited to 'modules/core/ollama-manager.js')
| -rw-r--r-- | modules/core/ollama-manager.js | 253 |
1 files changed, 146 insertions, 107 deletions
diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js index 420cbc3..ab3ec2c 100644 --- a/modules/core/ollama-manager.js +++ b/modules/core/ollama-manager.js @@ -13,6 +13,8 @@ class OllamaManager { this._abortController = null; // --- AI Agent Loop Optimization Toggle --- this.optimizationEnabled = localStorage.getItem('webx11_optimization_enabled') !== 'false'; + // --- Streaming Toggle --- + this.streamEnabled = localStorage.getItem('webx11_stream_enabled') !== 'false'; // --- Validator Agent Config (future) --- this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true'; this.validatorModel = localStorage.getItem('webx11_validator_model') || ''; @@ -359,48 +361,53 @@ class OllamaManager { async _chatWithProvider(provider, prompt, model) { const useModel = model || provider.model || this.currentModel; let fullText = ''; + this._abortController = new AbortController(); + const signal = this._abortController.signal; try { if (provider.type === 'ollama') { - this._abortController = new AbortController(); - const signal = this._abortController.signal; const response = await fetch(`${provider.endpoint}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ model: useModel, prompt: prompt, stream: true }), + body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled }), signal }); - this._abortController = null; if (!response.ok) throw new Error(`HTTP ${response.status}`); - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let isFirstToken = true; + if (this.streamEnabled) { + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let isFirstToken = true; - while (true) { - const { done, value } = await reader.read(); - if (done) break; + while (true) { + const { done, value } = await reader.read(); + if (done) break; - const chunk = decoder.decode(value, { stream: true }); - const lines = chunk.split('\n').filter(l => l.trim()); + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split('\n').filter(l => l.trim()); - for (const line of lines) { - try { - const parsed = JSON.parse(line); - if (parsed.response) { - fullText += parsed.response; - this.terminal.printStreamingResponse(parsed.response, !isFirstToken); - isFirstToken = false; + for (const line of lines) { + try { + const parsed = JSON.parse(line); + if (parsed.response) { + fullText += parsed.response; + this.terminal.printStreamingResponse(parsed.response, !isFirstToken); + isFirstToken = false; + } + } catch (e) { + // Skip malformed partial lines } - } catch (e) { - // Skip malformed partial lines } } + } else { + const data = await response.json(); + fullText = data.response || ''; + if (fullText) { + this.terminal.printStreamingResponse(fullText, false); + } } return fullText || null; } else { // OpenAI-compatible provider - this._abortController = new AbortController(); - const signal = this._abortController.signal; const headers = { 'Content-Type': 'application/json' }; if (provider.apiKey) { headers['Authorization'] = 'Bearer ' + provider.apiKey; @@ -411,43 +418,50 @@ class OllamaManager { body: JSON.stringify({ model: useModel, messages: [{ role: 'user', content: prompt }], - stream: true + stream: this.streamEnabled }), signal }); - this._abortController = null; if (!response.ok) throw new Error(`HTTP ${response.status}`); - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let isFirstToken = true; - let buffer = ''; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || trimmed === 'data: [DONE]') continue; - if (trimmed.startsWith('data: ')) { - try { - const parsed = JSON.parse(trimmed.substring(6)); - const content = parsed.choices?.[0]?.delta?.content || ''; - if (content) { - fullText += content; - this.terminal.printStreamingResponse(content, !isFirstToken); - isFirstToken = false; + if (this.streamEnabled) { + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let isFirstToken = true; + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed === 'data: [DONE]') continue; + if (trimmed.startsWith('data: ')) { + try { + const parsed = JSON.parse(trimmed.substring(6)); + const content = parsed.choices?.[0]?.delta?.content || ''; + if (content) { + fullText += content; + this.terminal.printStreamingResponse(content, !isFirstToken); + isFirstToken = false; + } + } catch (e) { + // Skip malformed SSE lines } - } catch (e) { - // Skip malformed SSE lines } } } + } else { + const data = await response.json(); + fullText = data.choices?.[0]?.message?.content || ''; + if (fullText) { + this.terminal.printStreamingResponse(fullText, false); + } } return fullText || null; } @@ -457,6 +471,8 @@ class OllamaManager { return fullText; } return null; // signal failure so caller can try next provider + } finally { + this._abortController = null; } } @@ -496,52 +512,58 @@ class OllamaManager { async _chatStreamWithProvider(provider, prompt, onToken, onComplete, model) { const useModel = model || provider.model || this.currentModel; + this._abortController = new AbortController(); + const signal = this._abortController.signal; try { if (provider.type === 'ollama') { - this._abortController = new AbortController(); - const signal = this._abortController.signal; const response = await fetch(`${provider.endpoint}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ model: useModel, prompt: prompt, stream: true }), + body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled }), signal }); - this._abortController = null; if (!response.ok) throw new Error(`HTTP ${response.status}`); - const reader = response.body.getReader(); - const decoder = new TextDecoder(); + let fullResponse = ''; - let buffer = ''; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - for (const line of lines) { - if (!line.trim()) continue; + if (this.streamEnabled) { + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + for (const line of lines) { + if (!line.trim()) continue; + try { + const obj = JSON.parse(line); + if (obj.response) { + fullResponse += obj.response; + onToken && onToken(obj.response, fullResponse); + } + } catch (e) { continue; } + } + } + if (buffer.trim()) { try { - const obj = JSON.parse(line); - if (obj.response) { - fullResponse += obj.response; - onToken && onToken(obj.response, fullResponse); - } - } catch (e) { continue; } + const obj = JSON.parse(buffer); + if (obj.response) { fullResponse += obj.response; onToken && onToken(obj.response, fullResponse); } + } catch (e) {} + } + } else { + const data = await response.json(); + fullResponse = data.response || ''; + if (fullResponse) { + onToken && onToken(fullResponse, fullResponse); } - } - if (buffer.trim()) { - try { - const obj = JSON.parse(buffer); - if (obj.response) { fullResponse += obj.response; onToken && onToken(obj.response, fullResponse); } - } catch (e) {} } this.fireResponseHooks(fullResponse); onComplete && onComplete(fullResponse); return fullResponse; } else { // OpenAI-compatible provider - this._abortController = new AbortController(); - const signal = this._abortController.signal; const headers = { 'Content-Type': 'application/json' }; if (provider.apiKey) { headers['Authorization'] = 'Bearer ' + provider.apiKey; @@ -552,38 +574,45 @@ class OllamaManager { body: JSON.stringify({ model: useModel, messages: [{ role: 'user', content: prompt }], - stream: true + stream: this.streamEnabled }), signal }); - this._abortController = null; if (!response.ok) throw new Error(`HTTP ${response.status}`); - const reader = response.body.getReader(); - const decoder = new TextDecoder(); let fullResponse = ''; - let buffer = ''; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || trimmed === 'data: [DONE]') continue; - if (trimmed.startsWith('data: ')) { - try { - const parsed = JSON.parse(trimmed.substring(6)); - const content = parsed.choices?.[0]?.delta?.content || ''; - if (content) { - fullResponse += content; - onToken && onToken(content, fullResponse); - } - } catch (e) { continue; } + if (this.streamEnabled) { + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed === 'data: [DONE]') continue; + if (trimmed.startsWith('data: ')) { + try { + const parsed = JSON.parse(trimmed.substring(6)); + const content = parsed.choices?.[0]?.delta?.content || ''; + if (content) { + fullResponse += content; + onToken && onToken(content, fullResponse); + } + } catch (e) { continue; } + } } } + } else { + const data = await response.json(); + fullResponse = data.choices?.[0]?.message?.content || ''; + if (fullResponse) { + onToken && onToken(fullResponse, fullResponse); + } } this.fireResponseHooks(fullResponse); onComplete && onComplete(fullResponse); @@ -593,6 +622,8 @@ class OllamaManager { this.terminal.print(`Stream error with ${provider.name}: ${error.message}`, 'error'); onComplete && onComplete(null); return null; + } finally { + this._abortController = null; } } @@ -654,7 +685,6 @@ class OllamaManager { body: JSON.stringify(body), signal }); - this._abortController = null; const networkTime = Math.round((Date.now() - t0) / 1000); if (!response.ok) { if (response.status === 400) { @@ -696,7 +726,6 @@ class OllamaManager { body: JSON.stringify(body), signal }); - this._abortController = null; const networkTime = Math.round((Date.now() - t0) / 1000); if (!response.ok) { throw new Error(`HTTP ${response.status}`); @@ -729,11 +758,13 @@ class OllamaManager { this.terminal.print('⚠️ DeepSeek returned empty — retrying with stream...', 'warning'); body.stream = true; delete body.stop; + this._abortController = new AbortController(); + const retrySignal = this._abortController.signal; const retryResponse = await fetch(`${provider.endpoint}/v1/chat/completions`, { method: 'POST', headers, body: JSON.stringify(body), - signal: this._abortController ? undefined : undefined + signal: retrySignal }); if (retryResponse.ok) { const reader = retryResponse.body.getReader(); @@ -907,6 +938,7 @@ class OllamaManager { localStorage.setItem('webx11_ollama_endpoint', this.endpoint); localStorage.setItem('webx11_ollama_model', this.currentModel); localStorage.setItem('webx11_optimization_enabled', this.optimizationEnabled ? 'true' : 'false'); + localStorage.setItem('webx11_stream_enabled', this.streamEnabled ? 'true' : 'false'); localStorage.setItem('webx11_validator_enabled', this.validatorEnabled ? 'true' : 'false'); localStorage.setItem('webx11_validator_model', this.validatorModel); localStorage.setItem('webx11_validator_endpoint', this.validatorEndpoint || ''); @@ -918,6 +950,13 @@ class OllamaManager { this.saveSettings(); } + setStreamEnabled(val) { + this.streamEnabled = !!val; + this.terminal?.print(`Response streaming ${this.streamEnabled ? 'enabled' : 'disabled'}.`, 'info'); + this.terminal?.updateStreamIndicator(); + this.saveSettings(); + } + setValidatorConfig(enabled, model) { this.validatorEnabled = !!enabled; if (model !== undefined) { |