mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
allow byok even when logged in
This commit is contained in:
parent
0b55dc3300
commit
c1fd4e6221
22 changed files with 452 additions and 221 deletions
|
|
@ -2,7 +2,6 @@ import type { EventConsumer, EventConsumerTarget } from '../events/consumer.js';
|
|||
import { routeBatch } from '../events/routing.js';
|
||||
import { createLanguageModel } from '../models/models.js';
|
||||
import {
|
||||
getDefaultModelAndProvider,
|
||||
getBackgroundTaskAgentModel,
|
||||
resolveProviderConfig,
|
||||
} from '../models/defaults.js';
|
||||
|
|
@ -10,8 +9,7 @@ import { listTasks } from './fileops.js';
|
|||
import { runBackgroundTask } from './runner.js';
|
||||
|
||||
async function resolveRoutingModel() {
|
||||
const modelId = await getBackgroundTaskAgentModel();
|
||||
const { provider } = await getDefaultModelAndProvider();
|
||||
const { model: modelId, provider } = await getBackgroundTaskAgentModel();
|
||||
const config = await resolveProviderConfig(provider);
|
||||
return {
|
||||
model: createLanguageModel(config, modelId, { priority: 'classifier' }),
|
||||
|
|
|
|||
|
|
@ -137,7 +137,12 @@ export async function runBackgroundTask(
|
|||
}
|
||||
}
|
||||
|
||||
const model = task.model || await getBackgroundTaskAgentModel();
|
||||
// task.yaml model/provider win; otherwise the category default
|
||||
// (provider-qualified in hybrid mode). A task model without a
|
||||
// provider keeps the legacy meaning: the app-default provider.
|
||||
const selection = await getBackgroundTaskAgentModel();
|
||||
const model = task.model || selection.model;
|
||||
const provider = task.provider ?? (task.model ? undefined : selection.provider);
|
||||
// Establish the use-case context for the whole turn so every tool the
|
||||
// agent calls (notably notify-user) reads `background_task_agent` via
|
||||
// getCurrentUseCase(); the AsyncLocalStorage context set here flows
|
||||
|
|
@ -148,7 +153,7 @@ export async function runBackgroundTask(
|
|||
agentId: 'background-task-agent',
|
||||
message: buildMessage(slug, task, trigger, context, codeProject),
|
||||
model,
|
||||
...(task.provider ? { provider: task.provider } : {}),
|
||||
...(provider ? { provider } : {}),
|
||||
throwOnError: true,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@ async function processAgentNotes(): Promise<void> {
|
|||
await runHeadlessAgent({
|
||||
agentId: AGENT_ID,
|
||||
message,
|
||||
model: await getKgModel(),
|
||||
...(await getKgModel()),
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -388,7 +388,7 @@ async function createNotesFromBatch(
|
|||
const { turnId, state } = await runHeadlessAgent({
|
||||
agentId: NOTE_CREATION_AGENT,
|
||||
message,
|
||||
model: await getKgModel(),
|
||||
...(await getKgModel()),
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
|
|
@ -946,7 +946,7 @@ export async function curateNotes(): Promise<void> {
|
|||
await runHeadlessAgent({
|
||||
agentId: CURATION_AGENT,
|
||||
message,
|
||||
model: await getKgModel(),
|
||||
...(await getKgModel()),
|
||||
throwOnError: true,
|
||||
});
|
||||
curated++;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { WorkDir } from '../config/config.js';
|
|||
import { createLanguageModel } from '../models/models.js';
|
||||
import { generateObjectSafe } from '../models/structured.js';
|
||||
import {
|
||||
getDefaultModelAndProvider,
|
||||
getKgModel,
|
||||
resolveProviderConfig,
|
||||
} from '../models/defaults.js';
|
||||
|
|
@ -246,8 +245,7 @@ export async function classifyThread(
|
|||
// (no-ops unless enough new corrections exist).
|
||||
await maybeDistillImportanceRules();
|
||||
|
||||
const modelId = await getKgModel();
|
||||
const { provider } = await getDefaultModelAndProvider();
|
||||
const { model: modelId, provider } = await getKgModel();
|
||||
const config = await resolveProviderConfig(provider);
|
||||
const model = createLanguageModel(config, modelId, { priority: 'background' });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { z } from 'zod';
|
||||
import { generateObject } from 'ai';
|
||||
import { WorkDir } from '../config/config.js';
|
||||
import { createProvider } from '../models/models.js';
|
||||
import { getDefaultModelAndProvider, getKgModel, resolveProviderConfig } from '../models/defaults.js';
|
||||
import { createLanguageModel } from '../models/models.js';
|
||||
import { generateObjectSafe } from '../models/structured.js';
|
||||
import { getKgModel, resolveProviderConfig } from '../models/defaults.js';
|
||||
import { captureLlmUsage } from '../analytics/usage.js';
|
||||
import { withUseCase } from '../analytics/use_case.js';
|
||||
|
||||
|
|
@ -145,21 +145,21 @@ export async function maybeDistillImportanceRules(): Promise<void> {
|
|||
if (newSince <= 0) return;
|
||||
|
||||
try {
|
||||
const modelId = await getKgModel();
|
||||
const { provider } = await getDefaultModelAndProvider();
|
||||
const { model: modelId, provider } = await getKgModel();
|
||||
const config = await resolveProviderConfig(provider);
|
||||
const model = createProvider(config).languageModel(modelId);
|
||||
const model = createLanguageModel(config, modelId, { priority: 'background' });
|
||||
|
||||
const correctionLines = fb.corrections.map(c =>
|
||||
`- From: ${c.from} | Subject: "${c.subject}" | classifier said ${c.agentVerdict}, user corrected to ${c.userVerdict}`
|
||||
).join('\n');
|
||||
const existingRules = fb.rules.length ? `\n\nCurrent rules (rewrite/merge as needed):\n${fb.rules.map(r => `- ${r}`).join('\n')}` : '';
|
||||
|
||||
const result = await withUseCase({ useCase: 'knowledge_sync', subUseCase: 'importance_rule_distiller' }, () => generateObject({
|
||||
const result = await withUseCase({ useCase: 'knowledge_sync', subUseCase: 'importance_rule_distiller' }, () => generateObjectSafe({
|
||||
model,
|
||||
system: `You maintain a short list of email-importance preference rules for one user, derived from their explicit corrections of an automated classifier. Write at most ${MAX_RULES} rules. Rules must GENERALIZE (sender domains, email types, topics) — never restate a single thread. Where corrections conflict, prefer the more recent. Keep rules that are still supported; drop ones the corrections no longer support.`,
|
||||
prompt: `Corrections (oldest first):\n${correctionLines}${existingRules}`,
|
||||
schema: DistilledRules,
|
||||
retry: true,
|
||||
}));
|
||||
|
||||
captureLlmUsage({
|
||||
|
|
|
|||
|
|
@ -483,7 +483,7 @@ async function processInlineTasks(): Promise<void> {
|
|||
const { summary: result } = await runHeadlessAgent({
|
||||
agentId: INLINE_TASK_AGENT,
|
||||
message,
|
||||
model: await getKgModel(),
|
||||
...(await getKgModel()),
|
||||
});
|
||||
if (result) {
|
||||
if (task.targetId) {
|
||||
|
|
@ -562,7 +562,7 @@ export async function processRowboatInstruction(
|
|||
const { summary: rawResponse } = await runHeadlessAgent({
|
||||
agentId: INLINE_TASK_AGENT,
|
||||
message,
|
||||
model: await getKgModel(),
|
||||
...(await getKgModel()),
|
||||
});
|
||||
if (!rawResponse) {
|
||||
return { instruction, schedule: null, scheduleLabel: null, response: null };
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ async function labelEmailBatch(
|
|||
const { turnId, state } = await runHeadlessAgent({
|
||||
agentId: LABELING_AGENT,
|
||||
message,
|
||||
model: await getKgModel(),
|
||||
...(await getKgModel()),
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ import { runLiveNoteAgent } from './runner.js';
|
|||
import type { EventConsumer, EventConsumerTarget } from '../../events/consumer.js';
|
||||
import { routeBatch } from '../../events/routing.js';
|
||||
import { createLanguageModel } from '../../models/models.js';
|
||||
import { getDefaultModelAndProvider, getLiveNoteAgentModel, resolveProviderConfig } from '../../models/defaults.js';
|
||||
import { getLiveNoteAgentModel, resolveProviderConfig } from '../../models/defaults.js';
|
||||
|
||||
async function resolveRoutingModel() {
|
||||
const modelId = await getLiveNoteAgentModel();
|
||||
const { provider } = await getDefaultModelAndProvider();
|
||||
const { model: modelId, provider } = await getLiveNoteAgentModel();
|
||||
const config = await resolveProviderConfig(provider);
|
||||
return {
|
||||
model: createLanguageModel(config, modelId, { priority: 'classifier' }),
|
||||
|
|
|
|||
|
|
@ -108,7 +108,12 @@ export async function runLiveNoteAgent(
|
|||
|
||||
const bodyBefore = await readNoteBody(filePath);
|
||||
|
||||
const model = live.model ?? await getLiveNoteAgentModel();
|
||||
// Note-frontmatter model/provider win; otherwise the category default
|
||||
// (provider-qualified in hybrid mode). A frontmatter model without a
|
||||
// provider keeps the legacy meaning: the app-default provider.
|
||||
const selection = await getLiveNoteAgentModel();
|
||||
const model = live.model ?? selection.model;
|
||||
const provider = live.provider ?? (live.model ? undefined : selection.provider);
|
||||
// The use-case context propagates to every tool the agent calls; the
|
||||
// granular trigger doubles as the sub-use-case (manual / cron /
|
||||
// window / event) so dashboards can break down what woke the agent.
|
||||
|
|
@ -118,7 +123,7 @@ export async function runLiveNoteAgent(
|
|||
agentId: 'live-note-agent',
|
||||
message: buildMessage(filePath, live, trigger, context),
|
||||
model,
|
||||
...(live.provider ? { provider: live.provider } : {}),
|
||||
...(provider ? { provider } : {}),
|
||||
throwOnError: true,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import path from 'node:path';
|
|||
import { generateText } from 'ai';
|
||||
import { WorkDir } from '../config/config.js';
|
||||
import { createLanguageModel } from '../models/models.js';
|
||||
import { getDefaultModelAndProvider, getMeetingNotesModel, resolveProviderConfig } from '../models/defaults.js';
|
||||
import { getMeetingNotesModel, resolveProviderConfig } from '../models/defaults.js';
|
||||
import { captureLlmUsage } from '../analytics/usage.js';
|
||||
import { withUseCase } from '../analytics/use_case.js';
|
||||
import { parseFrontmatter } from '../application/lib/parse-frontmatter.js';
|
||||
|
|
@ -175,8 +175,7 @@ async function generateBrief(event: CalendarEvent, ctx: Awaited<ReturnType<typeo
|
|||
});
|
||||
if (attendeeLines.length) parts.push(`Attendees:\n${attendeeLines.join('\n')}`);
|
||||
|
||||
const modelId = await getMeetingNotesModel();
|
||||
const { provider: providerName } = await getDefaultModelAndProvider();
|
||||
const { model: modelId, provider: providerName } = await getMeetingNotesModel();
|
||||
const providerConfig = await resolveProviderConfig(providerName);
|
||||
const model = createLanguageModel(providerConfig, modelId, { priority: 'background' });
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import { generateText } from 'ai';
|
||||
import { createLanguageModel } from '../models/models.js';
|
||||
import { getDefaultModelAndProvider, getMeetingNotesModel, resolveProviderConfig } from '../models/defaults.js';
|
||||
import { getMeetingNotesModel, resolveProviderConfig } from '../models/defaults.js';
|
||||
import { WorkDir } from '../config/config.js';
|
||||
import { captureLlmUsage } from '../analytics/usage.js';
|
||||
import { withUseCase } from '../analytics/use_case.js';
|
||||
|
|
@ -137,8 +137,7 @@ function loadCalendarEventContext(calendarEventJson: string): string {
|
|||
}
|
||||
|
||||
export async function summarizeMeeting(transcript: string, meetingStartTime?: string, calendarEventJson?: string): Promise<string> {
|
||||
const modelId = await getMeetingNotesModel();
|
||||
const { provider: providerName } = await getDefaultModelAndProvider();
|
||||
const { model: modelId, provider: providerName } = await getMeetingNotesModel();
|
||||
const providerConfig = await resolveProviderConfig(providerName);
|
||||
const model = createLanguageModel(providerConfig, modelId, { priority: 'background' });
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ async function tagNoteBatch(
|
|||
const { turnId, state } = await runHeadlessAgent({
|
||||
agentId: NOTE_TAGGING_AGENT,
|
||||
message,
|
||||
model: await getKgModel(),
|
||||
...(await getKgModel()),
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import z from "zod";
|
||||
import { LlmProvider } from "@x/shared/dist/models.js";
|
||||
import { LlmModelConfig, LlmProvider, ModelRef } from "@x/shared/dist/models.js";
|
||||
import { IModelConfigRepo } from "./repo.js";
|
||||
import { isSignedIn } from "../account/account.js";
|
||||
import container from "../di/container.js";
|
||||
|
|
@ -15,17 +15,42 @@ const SIGNED_IN_KG_MODEL = "google/gemini-3.1-flash-lite";
|
|||
const SIGNED_IN_LIVE_NOTE_AGENT_MODEL = "google/gemini-3.1-flash-lite";
|
||||
const SIGNED_IN_AUTO_PERMISSION_DECISION_MODEL = "google/gemini-3.1-flash-lite";
|
||||
|
||||
export type ModelSelection = z.infer<typeof ModelRef>;
|
||||
|
||||
async function readConfig(): Promise<z.infer<typeof LlmModelConfig> | null> {
|
||||
try {
|
||||
const repo = container.resolve<IModelConfigRepo>("modelConfigRepo");
|
||||
return await repo.getConfig();
|
||||
} catch {
|
||||
// Signed-in users may have no models.json at all.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The single source of truth for "what model+provider should we use when
|
||||
* the caller didn't specify and the agent didn't declare". Returns names only.
|
||||
* This is the only place that branches on signed-in state.
|
||||
* the caller didn't specify and the agent didn't declare".
|
||||
*
|
||||
* Resolution order (hybrid mode):
|
||||
* 1. `defaultSelection` — the user's explicit choice; may point at the
|
||||
* gateway ("rowboat") or any BYOK provider, and is honored in both modes
|
||||
* (a "rowboat" selection is skipped while signed out — it needs auth).
|
||||
* 2. Signed in → the curated gateway default.
|
||||
* 3. BYOK → the legacy top-level provider/model pair.
|
||||
*/
|
||||
export async function getDefaultModelAndProvider(): Promise<{ model: string; provider: string }> {
|
||||
if (await isSignedIn()) {
|
||||
const signedIn = await isSignedIn();
|
||||
const cfg = await readConfig();
|
||||
const selection = cfg?.defaultSelection;
|
||||
if (selection && (selection.provider !== "rowboat" || signedIn)) {
|
||||
return { model: selection.model, provider: selection.provider };
|
||||
}
|
||||
if (signedIn) {
|
||||
return { model: SIGNED_IN_DEFAULT_MODEL, provider: SIGNED_IN_DEFAULT_PROVIDER };
|
||||
}
|
||||
const repo = container.resolve<IModelConfigRepo>("modelConfigRepo");
|
||||
const cfg = await repo.getConfig();
|
||||
if (!cfg) {
|
||||
throw new Error("No model configuration found (models.json missing and not signed in)");
|
||||
}
|
||||
return { model: cfg.model, provider: cfg.provider.flavor };
|
||||
}
|
||||
|
||||
|
|
@ -62,48 +87,60 @@ export async function resolveProviderConfig(name: string): Promise<z.infer<typeo
|
|||
throw new Error(`Provider '${name}' is referenced but not configured`);
|
||||
}
|
||||
|
||||
// Per-category model resolution (hybrid mode):
|
||||
// 1. An explicit override wins in BOTH modes. Provider-qualified refs are
|
||||
// used as-is (a "rowboat" ref is skipped while signed out); legacy string
|
||||
// overrides pair with the BYOK provider they were configured against
|
||||
// (the top-level flavor), NOT the dynamic default — so a signed-in user's
|
||||
// local-model overrides keep routing to their local server.
|
||||
// 2. No override, signed in → the curated gateway model.
|
||||
// 3. No override, BYOK → the assistant default.
|
||||
async function getCategoryModel(
|
||||
category: "knowledgeGraphModel" | "meetingNotesModel" | "liveNoteAgentModel" | "autoPermissionDecisionModel",
|
||||
curatedModel: string,
|
||||
): Promise<ModelSelection> {
|
||||
const signedIn = await isSignedIn();
|
||||
const cfg = await readConfig();
|
||||
const override = cfg?.[category];
|
||||
if (override) {
|
||||
if (typeof override === "string") {
|
||||
if (cfg) {
|
||||
return { model: override, provider: cfg.provider.flavor };
|
||||
}
|
||||
} else if (override.provider !== "rowboat" || signedIn) {
|
||||
return { model: override.model, provider: override.provider };
|
||||
}
|
||||
}
|
||||
if (signedIn) {
|
||||
return { model: curatedModel, provider: SIGNED_IN_DEFAULT_PROVIDER };
|
||||
}
|
||||
return getDefaultModelAndProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* when they're the top-level of a run.
|
||||
*/
|
||||
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;
|
||||
export async function getKgModel(): Promise<ModelSelection> {
|
||||
return getCategoryModel("knowledgeGraphModel", SIGNED_IN_KG_MODEL);
|
||||
}
|
||||
|
||||
/** Model used by the live-note agent + routing classifier. */
|
||||
export async function getLiveNoteAgentModel(): Promise<ModelSelection> {
|
||||
return getCategoryModel("liveNoteAgentModel", SIGNED_IN_LIVE_NOTE_AGENT_MODEL);
|
||||
}
|
||||
|
||||
/** Model used by the auto-permission classifier. */
|
||||
export async function getAutoPermissionDecisionModel(): Promise<ModelSelection> {
|
||||
return getCategoryModel("autoPermissionDecisionModel", SIGNED_IN_AUTO_PERMISSION_DECISION_MODEL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Model used by the live-note agent + routing classifier.
|
||||
* Signed-in: curated default. BYOK: user override (`liveNoteAgentModel`) or
|
||||
* assistant model.
|
||||
* Model used by the meeting-notes summarizer. No special signed-in curated
|
||||
* model — historically meetings used the assistant model.
|
||||
*/
|
||||
export async function getLiveNoteAgentModel(): Promise<string> {
|
||||
if (await isSignedIn()) return SIGNED_IN_LIVE_NOTE_AGENT_MODEL;
|
||||
const cfg = await container.resolve<IModelConfigRepo>("modelConfigRepo").getConfig();
|
||||
return cfg.liveNoteAgentModel ?? cfg.model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model used by the auto-permission classifier.
|
||||
* Signed-in: curated default. BYOK: user override
|
||||
* (`autoPermissionDecisionModel`) or assistant model.
|
||||
*/
|
||||
export async function getAutoPermissionDecisionModel(): Promise<string> {
|
||||
if (await isSignedIn()) return SIGNED_IN_AUTO_PERMISSION_DECISION_MODEL;
|
||||
const cfg = await container.resolve<IModelConfigRepo>("modelConfigRepo").getConfig();
|
||||
return cfg.autoPermissionDecisionModel ?? 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;
|
||||
export async function getMeetingNotesModel(): Promise<ModelSelection> {
|
||||
return getCategoryModel("meetingNotesModel", SIGNED_IN_DEFAULT_MODEL);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -112,6 +149,6 @@ export async function getMeetingNotesModel(): Promise<string> {
|
|||
* agent model. Split into its own getter so a future per-feature override
|
||||
* doesn't require touching all call sites.
|
||||
*/
|
||||
export async function getBackgroundTaskAgentModel(): Promise<string> {
|
||||
export async function getBackgroundTaskAgentModel(): Promise<ModelSelection> {
|
||||
return getLiveNoteAgentModel();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,24 @@ import fs from "fs/promises";
|
|||
import path from "path";
|
||||
import z from "zod";
|
||||
|
||||
export type ModelConfigPatch = {
|
||||
[K in
|
||||
| "defaultSelection"
|
||||
| "knowledgeGraphModel"
|
||||
| "meetingNotesModel"
|
||||
| "liveNoteAgentModel"
|
||||
| "autoPermissionDecisionModel"]?: z.infer<typeof ModelConfig>[K] | null;
|
||||
};
|
||||
|
||||
export interface IModelConfigRepo {
|
||||
ensureConfig(): Promise<void>;
|
||||
getConfig(): Promise<z.infer<typeof ModelConfig>>;
|
||||
setConfig(config: z.infer<typeof ModelConfig>): Promise<void>;
|
||||
// Merge the given top-level keys into the existing file without touching
|
||||
// provider credentials — hybrid settings (default selection, category
|
||||
// overrides) save through this. Omitted keys are untouched; an explicit
|
||||
// null clears the key back to its default.
|
||||
updateConfig(patch: ModelConfigPatch): Promise<void>;
|
||||
}
|
||||
|
||||
const defaultConfig: z.infer<typeof ModelConfig> = {
|
||||
|
|
@ -63,7 +77,37 @@ export class FSModelConfigRepo implements IModelConfigRepo {
|
|||
autoPermissionDecisionModel: config.autoPermissionDecisionModel,
|
||||
};
|
||||
|
||||
const toWrite = { ...config, providers: existingProviders };
|
||||
// saveConfig owns provider credentials/model lists; the hybrid-mode
|
||||
// selections are owned by updateConfig — carry them over when the
|
||||
// incoming config doesn't set them.
|
||||
let existingSelections: Record<string, unknown> = {};
|
||||
try {
|
||||
const raw = await fs.readFile(this.configPath, "utf8");
|
||||
const existing = JSON.parse(raw);
|
||||
existingSelections = Object.fromEntries(
|
||||
["defaultSelection", "knowledgeGraphModel", "meetingNotesModel", "liveNoteAgentModel", "autoPermissionDecisionModel"]
|
||||
.filter((key) => existing[key] !== undefined && (config as Record<string, unknown>)[key] === undefined)
|
||||
.map((key) => [key, existing[key]]),
|
||||
);
|
||||
} catch {
|
||||
// No existing config
|
||||
}
|
||||
|
||||
const toWrite = { ...existingSelections, ...config, providers: existingProviders };
|
||||
await fs.writeFile(this.configPath, JSON.stringify(toWrite, null, 2));
|
||||
}
|
||||
|
||||
async updateConfig(patch: ModelConfigPatch): Promise<void> {
|
||||
const raw = await fs.readFile(this.configPath, "utf8");
|
||||
const existing = JSON.parse(raw) as Record<string, unknown>;
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
if (value === undefined || value === null) {
|
||||
delete existing[key];
|
||||
} else {
|
||||
existing[key] = value;
|
||||
}
|
||||
}
|
||||
ModelConfig.parse(existing);
|
||||
await fs.writeFile(this.configPath, JSON.stringify(existing, null, 2));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ Process new items and use the user context above to identify yourself when draft
|
|||
await runHeadlessAgent({
|
||||
agentId: agentName,
|
||||
message,
|
||||
model: await getKgModel(),
|
||||
...(await getKgModel()),
|
||||
});
|
||||
|
||||
// Update last run time
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { ToolPermissionMetadata } from "@x/shared/dist/runs.js";
|
|||
import { ToolCallPart } from "@x/shared/dist/message.js";
|
||||
import { captureLlmUsage } from "../analytics/usage.js";
|
||||
import { withUseCase, type UseCase } from "../analytics/use_case.js";
|
||||
import { getAutoPermissionDecisionModel, getDefaultModelAndProvider, resolveProviderConfig } from "../models/defaults.js";
|
||||
import { getAutoPermissionDecisionModel, resolveProviderConfig } from "../models/defaults.js";
|
||||
import { createLanguageModel } from "../models/models.js";
|
||||
import { generateObjectSafe } from "../models/structured.js";
|
||||
|
||||
|
|
@ -81,8 +81,7 @@ export async function classifyToolPermissions(input: {
|
|||
}): Promise<AutoPermissionDecision[]> {
|
||||
if (input.candidates.length === 0) return [];
|
||||
|
||||
const modelId = await getAutoPermissionDecisionModel();
|
||||
const { provider: providerName } = await getDefaultModelAndProvider();
|
||||
const { model: modelId, provider: providerName } = await getAutoPermissionDecisionModel();
|
||||
const providerConfig = await resolveProviderConfig(providerName);
|
||||
const model = createLanguageModel(providerConfig, modelId, { priority: "classifier" });
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue