configurable kg / meeting / track-block model overrides

Bring back per-category model selection that 5c4aa772 dropped, plus add a
new track-block category. Each is a BYOK-only override on `LlmModelConfig`
(`knowledgeGraphModel`, `meetingNotesModel`, `trackBlockModel`); signed-in
users always get the curated gateway default and never hit the on-disk
config.

Three helpers in core/models/defaults.ts — `getKgModel`,
`getTrackBlockModel`, `getMeetingNotesModel` — each check `isSignedIn`
first (fast path) and fall through to `cfg.<field> ?? cfg.model` for BYOK.

The model is now picked at the invocation site rather than via runtime
agent-name branching: each top-level `createRun` for a polling KG agent
or a track-block update passes `model: await getXxxModel()`. The `model:`
declarations on the affected agent YAMLs are dropped — they were dead
code under the per-call override. Standalone (non-run) callers
`track/routing` and `summarize_meeting` use the helpers inline.

Settings dialog and the two onboarding flows surface the two new fields
("Meeting Notes Model", "Track Block Model") next to the existing
"Knowledge Graph Model"; `repo.setConfig` persists all three per-provider.

Note: the signed-in `RowboatModelSettings` panel still has its
now-defunct kg selector; that's a UI cleanup for a later pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-04-24 16:44:02 +05:30
parent bdf270b7a1
commit caf00fae0c
22 changed files with 309 additions and 46 deletions

View file

@ -3,6 +3,7 @@ import path from 'path';
import { google } from 'googleapis';
import { WorkDir } from '../config/config.js';
import { createRun, createMessage } from '../runs/runs.js';
import { getKgModel } from '../models/defaults.js';
import { waitForRunCompletion } from '../agents/utils.js';
import { serviceLogger } from '../services/service_logger.js';
import { loadUserConfig, updateUserEmail } from '../config/user_config.js';
@ -305,7 +306,7 @@ async function processAgentNotes(): Promise<void> {
const timestamp = new Date().toISOString();
const message = `Current timestamp: ${timestamp}\n\nProcess the following source material and update the Agent Notes folder accordingly.\n\n${messageParts.join('\n\n')}`;
const agentRun = await createRun({ agentId: AGENT_ID });
const agentRun = await createRun({ agentId: AGENT_ID, model: await getKgModel() });
await createMessage(agentRun.id, message);
await waitForRunCompletion(agentRun.id);

View file

@ -1,6 +1,5 @@
export function getRaw(): string {
return `---
model: anthropic/claude-haiku-4.5
tools:
workspace-writeFile:
type: builtin

View file

@ -13,7 +13,6 @@ export function getRaw(): string {
const defaultEndISO = defaultEnd.toISOString();
return `---
model: anthropic/claude-sonnet-4.6
tools:
${toolEntries}
---

View file

@ -4,6 +4,7 @@ import { CronExpressionParser } from 'cron-parser';
import { generateText } from 'ai';
import { WorkDir } from '../config/config.js';
import { createRun, createMessage, fetchRun } from '../runs/runs.js';
import { getKgModel } from '../models/defaults.js';
import container from '../di/container.js';
import type { IModelConfigRepo } from '../models/repo.js';
import { createProvider } from '../models/models.js';
@ -467,7 +468,7 @@ async function processInlineTasks(): Promise<void> {
console.log(`[InlineTasks] Running task: "${task.instruction.slice(0, 80)}..."`);
try {
const run = await createRun({ agentId: INLINE_TASK_AGENT });
const run = await createRun({ agentId: INLINE_TASK_AGENT, model: await getKgModel() });
const message = [
`Execute the following instruction from the note "${relativePath}":`,
@ -547,7 +548,7 @@ export async function processRowboatInstruction(
scheduleLabel: string | null;
response: string | null;
}> {
const run = await createRun({ agentId: INLINE_TASK_AGENT });
const run = await createRun({ agentId: INLINE_TASK_AGENT, model: await getKgModel() });
const message = [
`Process the following @rowboat instruction from the note "${notePath}":`,

View file

@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import { WorkDir } from '../config/config.js';
import { createRun, createMessage } from '../runs/runs.js';
import { getKgModel } from '../models/defaults.js';
import { bus } from '../runs/bus.js';
import { waitForRunCompletion } from '../agents/utils.js';
import { serviceLogger } from '../services/service_logger.js';
@ -71,6 +72,7 @@ async function labelEmailBatch(
): Promise<{ runId: string; filesEdited: Set<string> }> {
const run = await createRun({
agentId: LABELING_AGENT,
model: await getKgModel(),
});
let message = `Label the following ${files.length} email files by prepending YAML frontmatter.\n\n`;

View file

@ -2,7 +2,6 @@ import { renderTagSystemForEmails } from './tag_system.js';
export function getRaw(): string {
return `---
model: anthropic/claude-haiku-4.5
tools:
workspace-readFile:
type: builtin

View file

@ -3,7 +3,6 @@ import { renderNoteEffectRules } from './tag_system.js';
export function getRaw(): string {
return `---
model: anthropic/claude-haiku-4.5
tools:
workspace-writeFile:
type: builtin

View file

@ -2,7 +2,6 @@ import { renderTagSystemForNotes } from './tag_system.js';
export function getRaw(): string {
return `---
model: anthropic/claude-haiku-4.5
tools:
workspace-readFile:
type: builtin

View file

@ -2,7 +2,7 @@ import fs from 'fs';
import path from 'path';
import { generateText } from 'ai';
import { createProvider } from '../models/models.js';
import { getDefaultModelAndProvider, resolveProviderConfig } from '../models/defaults.js';
import { getDefaultModelAndProvider, getMeetingNotesModel, resolveProviderConfig } from '../models/defaults.js';
import { WorkDir } from '../config/config.js';
const CALENDAR_SYNC_DIR = path.join(WorkDir, 'calendar_sync');
@ -135,7 +135,8 @@ function loadCalendarEventContext(calendarEventJson: string): string {
}
export async function summarizeMeeting(transcript: string, meetingStartTime?: string, calendarEventJson?: string): Promise<string> {
const { model: modelId, provider: providerName } = await getDefaultModelAndProvider();
const modelId = await getMeetingNotesModel();
const { provider: providerName } = await getDefaultModelAndProvider();
const providerConfig = await resolveProviderConfig(providerName);
const model = createProvider(providerConfig).languageModel(modelId);

View file

@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import { WorkDir } from '../config/config.js';
import { createRun, createMessage } from '../runs/runs.js';
import { getKgModel } from '../models/defaults.js';
import { bus } from '../runs/bus.js';
import { waitForRunCompletion } from '../agents/utils.js';
import { serviceLogger } from '../services/service_logger.js';
@ -84,6 +85,7 @@ async function tagNoteBatch(
): Promise<{ runId: string; filesEdited: Set<string> }> {
const run = await createRun({
agentId: NOTE_TAGGING_AGENT,
model: await getKgModel(),
});
let message = `Tag the following ${files.length} knowledge notes by prepending YAML frontmatter with appropriate tags.\n\n`;

View file

@ -2,7 +2,7 @@ import { generateObject } from 'ai';
import { trackBlock, PrefixLogger } from '@x/shared';
import type { KnowledgeEvent } from '@x/shared/dist/track-block.js';
import { createProvider } from '../../models/models.js';
import { getDefaultModelAndProvider, resolveProviderConfig } from '../../models/defaults.js';
import { getDefaultModelAndProvider, getTrackBlockModel, resolveProviderConfig } from '../../models/defaults.js';
const log = new PrefixLogger('TrackRouting');
@ -34,7 +34,8 @@ Rules:
- For each candidate, return BOTH trackId and filePath exactly as given. trackIds are not globally unique.`;
async function resolveModel() {
const { model, provider } = await getDefaultModelAndProvider();
const model = await getTrackBlockModel();
const { provider } = await getDefaultModelAndProvider();
const config = await resolveProviderConfig(provider);
return createProvider(config).languageModel(model);
}

View file

@ -1,6 +1,7 @@
import z from 'zod';
import { fetchAll, updateTrackBlock } from './fileops.js';
import { createRun, createMessage } from '../../runs/runs.js';
import { getTrackBlockModel } from '../../models/defaults.js';
import { extractAgentResponse, waitForRunCompletion } from '../../agents/utils.js';
import { trackBus } from './bus.js';
import type { TrackStateSchema } from './types.js';
@ -102,7 +103,7 @@ export async function triggerTrackUpdate(
const contentBefore = track.content;
// Emit start event — runId is set after agent run is created
const agentRun = await createRun({ agentId: 'track-run' });
const agentRun = await createRun({ agentId: 'track-run', model: await getTrackBlockModel() });
// Set lastRunAt and lastRunId immediately (before agent executes) so
// the scheduler's next poll won't re-trigger this track.

View file

@ -6,6 +6,8 @@ import container from "../di/container.js";
const SIGNED_IN_DEFAULT_MODEL = "gpt-5.4";
const SIGNED_IN_DEFAULT_PROVIDER = "rowboat";
const SIGNED_IN_KG_MODEL = "anthropic/claude-haiku-4.5";
const SIGNED_IN_TRACK_BLOCK_MODEL = "anthropic/claude-haiku-4.5";
/**
* The single source of truth for "what model+provider should we use when
@ -51,3 +53,36 @@ export async function resolveProviderConfig(name: string): Promise<z.infer<typeo
}
throw new Error(`Provider '${name}' is referenced but not configured`);
}
/**
* Model used by knowledge-graph agents (note_creation, labeling_agent, etc.)
* when they're the top-level of a run. Signed-in: curated default.
* BYOK: user override (`knowledgeGraphModel`) or assistant model.
*/
export async function getKgModel(): Promise<string> {
if (await isSignedIn()) return SIGNED_IN_KG_MODEL;
const cfg = await container.resolve<IModelConfigRepo>("modelConfigRepo").getConfig();
return cfg.knowledgeGraphModel ?? cfg.model;
}
/**
* Model used by track-block runner + routing classifier.
* Signed-in: curated default. BYOK: user override (`trackBlockModel`) or
* assistant model.
*/
export async function getTrackBlockModel(): Promise<string> {
if (await isSignedIn()) return SIGNED_IN_TRACK_BLOCK_MODEL;
const cfg = await container.resolve<IModelConfigRepo>("modelConfigRepo").getConfig();
return cfg.trackBlockModel ?? cfg.model;
}
/**
* Model used by the meeting-notes summarizer. No special signed-in default
* historically meetings used the assistant model. BYOK: user override
* (`meetingNotesModel`) or assistant model.
*/
export async function getMeetingNotesModel(): Promise<string> {
if (await isSignedIn()) return SIGNED_IN_DEFAULT_MODEL;
const cfg = await container.resolve<IModelConfigRepo>("modelConfigRepo").getConfig();
return cfg.meetingNotesModel ?? cfg.model;
}

View file

@ -52,6 +52,7 @@ export class FSModelConfigRepo implements IModelConfigRepo {
models: config.models,
knowledgeGraphModel: config.knowledgeGraphModel,
meetingNotesModel: config.meetingNotesModel,
trackBlockModel: config.trackBlockModel,
};
const toWrite = { ...config, providers: existingProviders };

View file

@ -1,5 +1,4 @@
---
model: anthropic/claude-haiku-4.5
tools:
workspace-readFile:
type: builtin

View file

@ -1,5 +1,4 @@
---
model: anthropic/claude-haiku-4.5
tools:
workspace-readFile:
type: builtin

View file

@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import { WorkDir } from '../config/config.js';
import { createRun, createMessage } from '../runs/runs.js';
import { getKgModel } from '../models/defaults.js';
import { waitForRunCompletion } from '../agents/utils.js';
import {
loadConfig,
@ -41,6 +42,7 @@ async function runAgent(agentName: string): Promise<void> {
// The agent file is expected to be in the agents directory with the same name
const run = await createRun({
agentId: agentName,
model: await getKgModel(),
});
// Build trigger message with user context