Add dual-model support with separate chat and agent models

Split the single model setting into `chatModel` and `agentModel` to allow
using different LLMs for conversational modes (Ask, Research) versus
agentic modes (Edit, Organize, Workflow, auto-organizer). Defaults are
`deepseek-v4-flash` for chat and `glm-5.1` for agents.

Includes backward compatibility migration from legacy `model` field,
updated settings UI, per-mode tool filtering via new `agent-modes.ts`
configs, and vault search scoring improvements (exact phrase, recency,
filename bonuses).
This commit is contained in:
2026-05-21 09:00:11 +02:00
parent ae747a4470
commit 96f201bf3f
7 changed files with 3310 additions and 303 deletions
+2 -1
View File
@@ -110,7 +110,8 @@ Open **Settings → Ollama Settings** to configure the plugin.
| Setting | Default | Description |
|---------|---------|-------------|
| Ollama URL | `http://localhost:11434` | Base URL of your Ollama instance |
| Model | `llama3` | Model used for chat responses |
| Chat Model | `deepseek-v4-flash` | Model used for normal chat, Ask mode, and Research mode |
| Agent Model | `glm-5.1` | Model used for Edit, Organize, Workflow, and auto-organizer tasks |
| **Default Agent Mode** | `Ask` | Default chat mode (Ask, Edit, Organize, Research, Workflow) |
| Vault Search Limit | `5` | Maximum number of vault entries to include in context |
| Max Context Length | `8000` | Maximum characters of vault content sent to the AI per message |
+3220 -265
View File
File diff suppressed because it is too large Load Diff
+53 -21
View File
@@ -67,12 +67,11 @@ export class ChatView extends ItemView {
this.listenersAttached = false;
this.settings = settings;
this.currentAgentMode = settings.agentMode ?? 'ask';
this.ollamaClient = new OllamaClient(
settings.ollamaUrl,
settings.model,
undefined,
settings.cacheConfig
);
this.ollamaClient = this.createOllamaClient(settings.chatModel ?? settings.model, settings);
this.agentOllamaClient =
(settings.agentModel ?? settings.model) === (settings.chatModel ?? settings.model)
? this.ollamaClient
: this.createOllamaClient(settings.agentModel ?? settings.model, settings);
this.vaultIndexer = new VaultIndexer(this.app.vault, undefined, vectorStore);
this.toolExecutor = new ToolExecutor(this.app.vault, this.app, telemetryManager);
this.actionPreviewBuilder = new ActionPreviewBuilder(this.app.vault, this.app);
@@ -84,7 +83,7 @@ export class ChatView extends ItemView {
this.app.vault,
this.app,
settings.ollamaUrl,
settings.model,
settings.agentModel ?? settings.model,
{ cacheConfig: settings.cacheConfig }
);
}
@@ -95,20 +94,22 @@ export class ChatView extends ItemView {
if (this.modeSelectorEl) {
this.modeSelectorEl.value = this.currentAgentMode;
}
this.ollamaClient = new OllamaClient(
newSettings.ollamaUrl,
newSettings.model,
undefined,
newSettings.cacheConfig
this.ollamaClient = this.createOllamaClient(
newSettings.chatModel ?? newSettings.model,
newSettings
);
this.agentOllamaClient =
(newSettings.agentModel ?? newSettings.model) === (newSettings.chatModel ?? newSettings.model)
? this.ollamaClient
: this.createOllamaClient(newSettings.agentModel ?? newSettings.model, newSettings);
this.workflowEngine = new WorkflowEngine(
this.app.vault,
this.app,
newSettings.ollamaUrl,
newSettings.model,
newSettings.agentModel ?? newSettings.model,
{ cacheConfig: newSettings.cacheConfig }
);
void this.ollamaClient.initializeCache().catch(() => {
void this.initializeClientCaches().catch(() => {
new Notice(
'Semantic cache unavailable: could not connect to ChromaDB. Check the ChromaDB URL in settings.'
);
@@ -122,6 +123,9 @@ export class ChatView extends ItemView {
async clearCache(): Promise<void> {
await this.ollamaClient.clearCache();
if (this.agentOllamaClient !== this.ollamaClient) {
await this.agentOllamaClient.clearCache();
}
}
getViewType(): string {
@@ -138,7 +142,7 @@ export class ChatView extends ItemView {
async onOpen(): Promise<void> {
try {
await this.ollamaClient.initializeCache();
await this.initializeClientCaches();
} catch {
new Notice(
'Semantic cache unavailable: could not connect to ChromaDB. Check the ChromaDB URL in settings.'
@@ -744,7 +748,7 @@ export class ChatView extends ItemView {
if (followUpMessages.length > 0) {
const finalMessages = [...messages, followUp, ...followUpMessages];
const followUpStartTime = Date.now();
const response = await this.ollamaClient.chat(finalMessages, tools);
const response = await this.getActiveOllamaClient().chat(finalMessages, tools);
const followUpDurationMs = Date.now() - followUpStartTime;
const finalResponse = response.content || fullResponse;
this.updateMessageById(assistantMessageId, {
@@ -755,7 +759,7 @@ export class ChatView extends ItemView {
// Record follow-up LLM call telemetry
this.telemetryManager?.recordLlmCall({
model: this.settings.model,
model: this.getActiveModel(),
promptTokens: Math.round(finalMessages.reduce((sum, m) => sum + m.content.length, 0) / 4),
completionTokens: Math.round(finalResponse.length / 4),
totalTokens: Math.round(
@@ -812,7 +816,7 @@ export class ChatView extends ItemView {
if (followUpMessages.length > 0) {
const finalMessages = [...messages, followUp, ...followUpMessages];
const followUpStartTime = Date.now();
const response = await this.ollamaClient.chat(finalMessages, tools);
const response = await this.getActiveOllamaClient().chat(finalMessages, tools);
const followUpDurationMs = Date.now() - followUpStartTime;
const finalResponse = response.content || 'Actions applied successfully.';
this.updateMessageById(assistantMessageId, {
@@ -823,7 +827,7 @@ export class ChatView extends ItemView {
// Record follow-up LLM call telemetry
this.telemetryManager?.recordLlmCall({
model: this.settings.model,
model: this.getActiveModel(),
promptTokens: Math.round(finalMessages.reduce((sum, m) => sum + m.content.length, 0) / 4),
completionTokens: Math.round(finalResponse.length / 4),
totalTokens: Math.round(
@@ -1090,7 +1094,9 @@ export class ChatView extends ItemView {
// Prepend structured memory as a system message if available
const messagesWithMemory = this.buildMessagesWithMemory(completeMessages);
const stream = this.ollamaClient.streamChat(messagesWithMemory, tools);
const activeClient = this.getActiveOllamaClient();
const activeModel = this.getActiveModel();
const stream = activeClient.streamChat(messagesWithMemory, tools);
let fullResponse = '';
let toolCalls: OllamaToolCall[] = [];
@@ -1131,7 +1137,7 @@ export class ChatView extends ItemView {
completionTokens > 0 ? completionTokens : fullResponse.length / 4;
this.telemetryManager?.recordLlmCall({
model: this.settings.model,
model: activeModel,
promptTokens: Math.round(estimatedPromptTokens),
completionTokens: Math.round(estimatedCompletionTokens),
totalTokens: Math.round(estimatedPromptTokens + estimatedCompletionTokens),
@@ -1232,6 +1238,7 @@ export class ChatView extends ItemView {
private listenersAttached: boolean = false;
private settings: PluginSettings;
private ollamaClient: OllamaClient;
private agentOllamaClient: OllamaClient;
private vaultIndexer: VaultIndexer;
private toolExecutor: ToolExecutor;
private actionPreviewBuilder: ActionPreviewBuilder;
@@ -1253,6 +1260,31 @@ export class ChatView extends ItemView {
tools: OllamaTool[];
assistantMessageId: string;
} | null = null;
private createOllamaClient(model: string, settings: PluginSettings): OllamaClient {
return new OllamaClient(settings.ollamaUrl, model, undefined, settings.cacheConfig);
}
private async initializeClientCaches(): Promise<void> {
await this.ollamaClient.initializeCache();
if (this.agentOllamaClient !== this.ollamaClient) {
await this.agentOllamaClient.initializeCache();
}
}
private getActiveModel(): string {
return this.isAgenticMode(this.currentAgentMode)
? (this.settings.agentModel ?? this.settings.model)
: (this.settings.chatModel ?? this.settings.model);
}
private getActiveOllamaClient(): OllamaClient {
return this.isAgenticMode(this.currentAgentMode) ? this.agentOllamaClient : this.ollamaClient;
}
private isAgenticMode(mode: AgentMode): boolean {
return mode === 'edit' || mode === 'organize' || mode === 'workflow';
}
}
const MAX_TOOL_CALLS = 5;
+3 -1
View File
@@ -1,6 +1,8 @@
export const DEFAULT_SETTINGS = {
ollamaUrl: 'http://localhost:11434',
model: 'llama3',
chatModel: 'deepseek-v4-flash',
agentModel: 'glm-5.1',
model: 'deepseek-v4-flash',
vaultSearchLimit: 5,
maxMessageHistory: 50,
maxContextLength: 8000,
+28 -15
View File
@@ -173,6 +173,10 @@ export default class OllamaPlugin extends Plugin {
// Backward compatibility: old flat format vs new nested format
const loadedSettings = (data.settings ?? data) as Partial<PluginSettings>;
this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedSettings);
const legacyModel = loadedSettings.model ?? DEFAULT_SETTINGS.model;
this.settings.chatModel = loadedSettings.chatModel ?? legacyModel;
this.settings.agentModel = loadedSettings.agentModel ?? legacyModel;
this.settings.model = this.settings.chatModel;
const memoryData: StructuredMemoryData =
(data.structuredMemory as StructuredMemoryData | undefined) ??
@@ -197,17 +201,13 @@ export default class OllamaPlugin extends Plugin {
}
initializeAutoOrganizer(): void {
if (!this.autoTagger) {
this.autoTagger = new AutoTagger(
this.app.vault,
this.app,
this.settings.ollamaUrl,
this.settings.model,
this.settings.autoTagConfig
);
} else {
this.autoTagger.updateConfig(this.settings.autoTagConfig);
}
this.autoTagger = new AutoTagger(
this.app.vault,
this.app,
this.settings.ollamaUrl,
this.settings.agentModel,
this.settings.autoTagConfig
);
if (!this.autoLinker) {
const vaultIndexer = new VaultIndexer(this.app.vault, undefined, this.vaultVectorStore);
@@ -448,12 +448,25 @@ class OllamaSettingTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName('Model')
.setDesc('Ollama model to use (default: llama3)')
.setName('Chat Model')
.setDesc('Model for normal chat, Ask mode, and Research mode (default: deepseek-v4-flash)')
.addText((text) =>
text.setValue(this.plugin.settings.model).onChange(async (value) => {
this.plugin.settings.model = value;
text.setValue(this.plugin.settings.chatModel).onChange(async (value) => {
this.plugin.settings.chatModel = value.trim();
this.plugin.settings.model = this.plugin.settings.chatModel;
await this.plugin.saveSettings();
this.plugin.notifyChatViews();
})
);
new Setting(containerEl)
.setName('Agent Model')
.setDesc('Model for Edit, Organize, Workflow, and auto-organizer tasks (default: glm-5.1)')
.addText((text) =>
text.setValue(this.plugin.settings.agentModel).onChange(async (value) => {
this.plugin.settings.agentModel = value.trim();
await this.plugin.saveSettings();
this.plugin.notifyChatViews();
})
);
+2
View File
@@ -314,6 +314,8 @@ export interface VaultIndexConfig {
export interface PluginSettings {
ollamaUrl: string;
chatModel: string;
agentModel: string;
model: string;
vaultSearchLimit: number;
maxMessageHistory: number;
+2
View File
@@ -33,6 +33,8 @@ jest.mock('obsidian', () => ({
const mockSettings: PluginSettings = {
ollamaUrl: 'http://localhost:11434',
chatModel: 'llama3',
agentModel: 'llama3',
model: 'llama3',
vaultSearchLimit: 3,
maxMessageHistory: 50,