aboutsummaryrefslogtreecommitdiff
path: root/modules/core/ollama-manager.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core/ollama-manager.js')
-rw-r--r--modules/core/ollama-manager.js83
1 files changed, 81 insertions, 2 deletions
diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js
index c790e6f..3f46b13 100644
--- a/modules/core/ollama-manager.js
+++ b/modules/core/ollama-manager.js
@@ -592,6 +592,12 @@ class OllamaManager {
// ─── Chat With Tools ─────────────────────────────────────────────────────
+ _isModelType(modelName, type) {
+ if (!modelName) return false;
+ const lower = modelName.toLowerCase();
+ return lower.includes(type);
+ }
+
async chatWithTools(messages, tools, model = null) {
if (!this.connected) {
this.terminal.print('Ollama not connected. Use ollamaconnect first.', 'error');
@@ -658,7 +664,8 @@ class OllamaManager {
error: null
};
} else {
- // OpenAI-compatible provider
+ // OpenAI-compatible provider (OpenRouter, DeepSeek, etc.)
+ const isDeepSeek = this._isModelType(useModel, 'deepseek');
const headers = { 'Content-Type': 'application/json' };
if (provider.apiKey) {
headers['Authorization'] = 'Bearer ' + provider.apiKey;
@@ -668,6 +675,11 @@ class OllamaManager {
messages: messages,
stream: false
};
+ if (isDeepSeek) {
+ body.temperature = 0.3;
+ body.max_tokens = 4096;
+ body.stop = ['<|end|>'];
+ }
if (tools && tools.length > 0) {
body.tools = tools;
}
@@ -688,8 +700,75 @@ class OllamaManager {
const choice = data.choices?.[0];
const message = choice?.message || null;
const toolCalls = message?.tool_calls || null;
+ const content = message?.content || '';
+
+ // DeepSeek: handle null content with tool calls — send empty string
+ if (isDeepSeek && content === '' && toolCalls && toolCalls.length > 0) {
+ return {
+ message: { role: 'assistant', content: '' },
+ tool_calls: toolCalls.map(tc => ({
+ id: tc.id,
+ type: tc.type || 'function',
+ function: {
+ name: tc.function?.name || '',
+ arguments: tc.function?.arguments || '{}'
+ }
+ })),
+ error: null
+ };
+ }
+
+ // DeepSeek: empty response with no tools — retry once with stream forcing
+ if (isDeepSeek && content === '' && (!toolCalls || toolCalls.length === 0)) {
+ this.terminal.print('⚠️ DeepSeek returned empty — retrying with stream...', 'warning');
+ body.stream = true;
+ delete body.stop;
+ const retryResponse = await fetch(`${provider.endpoint}/v1/chat/completions`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(body),
+ signal: this._abortController ? undefined : undefined
+ });
+ if (retryResponse.ok) {
+ const reader = retryResponse.body.getReader();
+ const decoder = new TextDecoder();
+ let fullText = '';
+ let retryBuffer = '';
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ retryBuffer += decoder.decode(value, { stream: true });
+ const lines = retryBuffer.split('\n');
+ retryBuffer = 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 chunk = parsed.choices?.[0]?.delta?.content || '';
+ if (chunk) fullText += chunk;
+ } catch (e) {}
+ }
+ }
+ }
+ if (fullText) {
+ return {
+ message: { role: 'assistant', content: fullText },
+ tool_calls: null,
+ error: null
+ };
+ }
+ }
+ return {
+ message: { role: 'assistant', content: '' },
+ tool_calls: null,
+ error: null
+ };
+ }
+
return {
- message: message ? { role: message.role || 'assistant', content: message.content || '' } : null,
+ message: message ? { role: message.role || 'assistant', content: content } : null,
tool_calls: toolCalls ? toolCalls.map(tc => ({
id: tc.id,
type: tc.type || 'function',