aboutsummaryrefslogtreecommitdiff
path: root/modules/core/ollama-manager.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-05-31 06:00:55 +0200
committerAgando <roduccomm@gmail.com>2026-05-31 06:00:55 +0200
commit7cc82d2d32e44ab1c5b7d042d5522d036d9aa0be (patch)
tree0a64e32f8b77043677e62485d7a0ecdf77e5c2d3 /modules/core/ollama-manager.js
parentd8bf7fa398d6d928325be9b5c64ca24875bfeef8 (diff)
auto: 2026-05-31_06:00 changes
Diffstat (limited to 'modules/core/ollama-manager.js')
-rw-r--r--modules/core/ollama-manager.js253
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) {