mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
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:
parent
bdf270b7a1
commit
caf00fae0c
22 changed files with 309 additions and 46 deletions
|
|
@ -59,14 +59,14 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
const [modelsCatalog, setModelsCatalog] = useState<Record<string, LlmModelOption[]>>({})
|
||||
const [modelsLoading, setModelsLoading] = useState(false)
|
||||
const [modelsError, setModelsError] = useState<string | null>(null)
|
||||
const [providerConfigs, setProviderConfigs] = useState<Record<LlmProviderFlavor, { apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string }>>({
|
||||
openai: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
||||
anthropic: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
||||
google: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
||||
openrouter: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
||||
aigateway: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
||||
ollama: { apiKey: "", baseURL: "http://localhost:11434", model: "", knowledgeGraphModel: "" },
|
||||
"openai-compatible": { apiKey: "", baseURL: "http://localhost:1234/v1", model: "", knowledgeGraphModel: "" },
|
||||
const [providerConfigs, setProviderConfigs] = useState<Record<LlmProviderFlavor, { apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string; meetingNotesModel: string; trackBlockModel: string }>>({
|
||||
openai: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
anthropic: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
google: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
openrouter: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
aigateway: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
ollama: { apiKey: "", baseURL: "http://localhost:11434", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
"openai-compatible": { apiKey: "", baseURL: "http://localhost:1234/v1", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
})
|
||||
const [testState, setTestState] = useState<{ status: "idle" | "testing" | "success" | "error"; error?: string }>({
|
||||
status: "idle",
|
||||
|
|
@ -109,7 +109,7 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
const [googleCalendarConnecting, setGoogleCalendarConnecting] = useState(false)
|
||||
|
||||
const updateProviderConfig = useCallback(
|
||||
(provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string }>) => {
|
||||
(provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string; meetingNotesModel: string; trackBlockModel: string }>) => {
|
||||
setProviderConfigs(prev => ({
|
||||
...prev,
|
||||
[provider]: { ...prev[provider], ...updates },
|
||||
|
|
@ -458,6 +458,8 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
const baseURL = activeConfig.baseURL.trim() || undefined
|
||||
const model = activeConfig.model.trim()
|
||||
const knowledgeGraphModel = activeConfig.knowledgeGraphModel.trim() || undefined
|
||||
const meetingNotesModel = activeConfig.meetingNotesModel.trim() || undefined
|
||||
const trackBlockModel = activeConfig.trackBlockModel.trim() || undefined
|
||||
const providerConfig = {
|
||||
provider: {
|
||||
flavor: llmProvider,
|
||||
|
|
@ -466,6 +468,8 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
},
|
||||
model,
|
||||
knowledgeGraphModel,
|
||||
meetingNotesModel,
|
||||
trackBlockModel,
|
||||
}
|
||||
const result = await window.ipc.invoke("models:test", providerConfig)
|
||||
if (result.success) {
|
||||
|
|
@ -1157,6 +1161,72 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Meeting notes model</span>
|
||||
{modelsLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Loading...
|
||||
</div>
|
||||
) : showModelInput ? (
|
||||
<Input
|
||||
value={activeConfig.meetingNotesModel}
|
||||
onChange={(e) => updateProviderConfig(llmProvider, { meetingNotesModel: e.target.value })}
|
||||
placeholder={activeConfig.model || "Enter model"}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
value={activeConfig.meetingNotesModel || "__same__"}
|
||||
onValueChange={(value) => updateProviderConfig(llmProvider, { meetingNotesModel: value === "__same__" ? "" : value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__same__">Same as assistant</SelectItem>
|
||||
{modelsForProvider.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.name || model.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Track block model</span>
|
||||
{modelsLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Loading...
|
||||
</div>
|
||||
) : showModelInput ? (
|
||||
<Input
|
||||
value={activeConfig.trackBlockModel}
|
||||
onChange={(e) => updateProviderConfig(llmProvider, { trackBlockModel: e.target.value })}
|
||||
placeholder={activeConfig.model || "Enter model"}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
value={activeConfig.trackBlockModel || "__same__"}
|
||||
onValueChange={(value) => updateProviderConfig(llmProvider, { trackBlockModel: value === "__same__" ? "" : value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__same__">Same as assistant</SelectItem>
|
||||
{modelsForProvider.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.name || model.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showApiKey && (
|
||||
|
|
|
|||
|
|
@ -221,6 +221,76 @@ export function LlmSetupStep({ state }: LlmSetupStepProps) {
|
|||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 min-w-0">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Meeting Notes Model
|
||||
</label>
|
||||
{modelsLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Loading...
|
||||
</div>
|
||||
) : showModelInput ? (
|
||||
<Input
|
||||
value={activeConfig.meetingNotesModel}
|
||||
onChange={(e) => updateProviderConfig(llmProvider, { meetingNotesModel: e.target.value })}
|
||||
placeholder={activeConfig.model || "Enter model"}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
value={activeConfig.meetingNotesModel || "__same__"}
|
||||
onValueChange={(value) => updateProviderConfig(llmProvider, { meetingNotesModel: value === "__same__" ? "" : value })}
|
||||
>
|
||||
<SelectTrigger className="w-full truncate">
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__same__">Same as assistant</SelectItem>
|
||||
{modelsForProvider.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.name || model.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 min-w-0">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Track Block Model
|
||||
</label>
|
||||
{modelsLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Loading...
|
||||
</div>
|
||||
) : showModelInput ? (
|
||||
<Input
|
||||
value={activeConfig.trackBlockModel}
|
||||
onChange={(e) => updateProviderConfig(llmProvider, { trackBlockModel: e.target.value })}
|
||||
placeholder={activeConfig.model || "Enter model"}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
value={activeConfig.trackBlockModel || "__same__"}
|
||||
onValueChange={(value) => updateProviderConfig(llmProvider, { trackBlockModel: value === "__same__" ? "" : value })}
|
||||
>
|
||||
<SelectTrigger className="w-full truncate">
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__same__">Same as assistant</SelectItem>
|
||||
{modelsForProvider.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.name || model.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showApiKey && (
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
|||
const [modelsCatalog, setModelsCatalog] = useState<Record<string, LlmModelOption[]>>({})
|
||||
const [modelsLoading, setModelsLoading] = useState(false)
|
||||
const [modelsError, setModelsError] = useState<string | null>(null)
|
||||
const [providerConfigs, setProviderConfigs] = useState<Record<LlmProviderFlavor, { apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string }>>({
|
||||
openai: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
||||
anthropic: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
||||
google: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
||||
openrouter: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
||||
aigateway: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
||||
ollama: { apiKey: "", baseURL: "http://localhost:11434", model: "", knowledgeGraphModel: "" },
|
||||
"openai-compatible": { apiKey: "", baseURL: "http://localhost:1234/v1", model: "", knowledgeGraphModel: "" },
|
||||
const [providerConfigs, setProviderConfigs] = useState<Record<LlmProviderFlavor, { apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string; meetingNotesModel: string; trackBlockModel: string }>>({
|
||||
openai: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
anthropic: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
google: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
openrouter: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
aigateway: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
ollama: { apiKey: "", baseURL: "http://localhost:11434", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
"openai-compatible": { apiKey: "", baseURL: "http://localhost:1234/v1", model: "", knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
})
|
||||
const [testState, setTestState] = useState<{ status: "idle" | "testing" | "success" | "error"; error?: string }>({
|
||||
status: "idle",
|
||||
|
|
@ -81,7 +81,7 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
|||
const [googleCalendarConnecting, setGoogleCalendarConnecting] = useState(false)
|
||||
|
||||
const updateProviderConfig = useCallback(
|
||||
(provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string }>) => {
|
||||
(provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string; meetingNotesModel: string; trackBlockModel: string }>) => {
|
||||
setProviderConfigs(prev => ({
|
||||
...prev,
|
||||
[provider]: { ...prev[provider], ...updates },
|
||||
|
|
@ -435,6 +435,8 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
|||
const baseURL = activeConfig.baseURL.trim() || undefined
|
||||
const model = activeConfig.model.trim()
|
||||
const knowledgeGraphModel = activeConfig.knowledgeGraphModel.trim() || undefined
|
||||
const meetingNotesModel = activeConfig.meetingNotesModel.trim() || undefined
|
||||
const trackBlockModel = activeConfig.trackBlockModel.trim() || undefined
|
||||
const providerConfig = {
|
||||
provider: {
|
||||
flavor: llmProvider,
|
||||
|
|
@ -443,6 +445,8 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
|||
},
|
||||
model,
|
||||
knowledgeGraphModel,
|
||||
meetingNotesModel,
|
||||
trackBlockModel,
|
||||
}
|
||||
const result = await window.ipc.invoke("models:test", providerConfig)
|
||||
if (result.success) {
|
||||
|
|
@ -459,7 +463,7 @@ export function useOnboardingState(open: boolean, onComplete: () => void) {
|
|||
setTestState({ status: "error", error: "Connection test failed" })
|
||||
toast.error("Connection test failed")
|
||||
}
|
||||
}, [activeConfig.apiKey, activeConfig.baseURL, activeConfig.model, activeConfig.knowledgeGraphModel, canTest, llmProvider, handleNext])
|
||||
}, [activeConfig.apiKey, activeConfig.baseURL, activeConfig.model, activeConfig.knowledgeGraphModel, activeConfig.meetingNotesModel, activeConfig.trackBlockModel, canTest, llmProvider, handleNext])
|
||||
|
||||
// Check connection status for all providers
|
||||
const refreshAllStatuses = useCallback(async () => {
|
||||
|
|
|
|||
|
|
@ -196,14 +196,14 @@ const defaultBaseURLs: Partial<Record<LlmProviderFlavor, string>> = {
|
|||
function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||
const [provider, setProvider] = useState<LlmProviderFlavor>("openai")
|
||||
const [defaultProvider, setDefaultProvider] = useState<LlmProviderFlavor | null>(null)
|
||||
const [providerConfigs, setProviderConfigs] = useState<Record<LlmProviderFlavor, { apiKey: string; baseURL: string; models: string[]; knowledgeGraphModel: string }>>({
|
||||
openai: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "" },
|
||||
anthropic: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "" },
|
||||
google: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "" },
|
||||
openrouter: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "" },
|
||||
aigateway: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "" },
|
||||
ollama: { apiKey: "", baseURL: "http://localhost:11434", models: [""], knowledgeGraphModel: "" },
|
||||
"openai-compatible": { apiKey: "", baseURL: "http://localhost:1234/v1", models: [""], knowledgeGraphModel: "" },
|
||||
const [providerConfigs, setProviderConfigs] = useState<Record<LlmProviderFlavor, { apiKey: string; baseURL: string; models: string[]; knowledgeGraphModel: string; meetingNotesModel: string; trackBlockModel: string }>>({
|
||||
openai: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
anthropic: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
google: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
openrouter: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
aigateway: { apiKey: "", baseURL: "", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
ollama: { apiKey: "", baseURL: "http://localhost:11434", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
"openai-compatible": { apiKey: "", baseURL: "http://localhost:1234/v1", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
})
|
||||
const [modelsCatalog, setModelsCatalog] = useState<Record<string, LlmModelOption[]>>({})
|
||||
const [modelsLoading, setModelsLoading] = useState(false)
|
||||
|
|
@ -229,7 +229,7 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
(!requiresBaseURL || activeConfig.baseURL.trim().length > 0)
|
||||
|
||||
const updateConfig = useCallback(
|
||||
(prov: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; models: string[]; knowledgeGraphModel: string }>) => {
|
||||
(prov: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; models: string[]; knowledgeGraphModel: string; meetingNotesModel: string; trackBlockModel: string }>) => {
|
||||
setProviderConfigs(prev => ({
|
||||
...prev,
|
||||
[prov]: { ...prev[prov], ...updates },
|
||||
|
|
@ -302,6 +302,8 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
baseURL: e.baseURL || (defaultBaseURLs[key as LlmProviderFlavor] || ""),
|
||||
models: savedModels,
|
||||
knowledgeGraphModel: e.knowledgeGraphModel || "",
|
||||
meetingNotesModel: e.meetingNotesModel || "",
|
||||
trackBlockModel: e.trackBlockModel || "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -318,6 +320,8 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
baseURL: parsed.provider.baseURL || (defaultBaseURLs[flavor] || ""),
|
||||
models: activeModels.length > 0 ? activeModels : [""],
|
||||
knowledgeGraphModel: parsed.knowledgeGraphModel || "",
|
||||
meetingNotesModel: parsed.meetingNotesModel || "",
|
||||
trackBlockModel: parsed.trackBlockModel || "",
|
||||
};
|
||||
}
|
||||
return next;
|
||||
|
|
@ -391,6 +395,8 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
model: allModels[0] || "",
|
||||
models: allModels,
|
||||
knowledgeGraphModel: activeConfig.knowledgeGraphModel.trim() || undefined,
|
||||
meetingNotesModel: activeConfig.meetingNotesModel.trim() || undefined,
|
||||
trackBlockModel: activeConfig.trackBlockModel.trim() || undefined,
|
||||
}
|
||||
const result = await window.ipc.invoke("models:test", providerConfig)
|
||||
if (result.success) {
|
||||
|
|
@ -423,6 +429,8 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
model: allModels[0],
|
||||
models: allModels,
|
||||
knowledgeGraphModel: config.knowledgeGraphModel.trim() || undefined,
|
||||
meetingNotesModel: config.meetingNotesModel.trim() || undefined,
|
||||
trackBlockModel: config.trackBlockModel.trim() || undefined,
|
||||
})
|
||||
setDefaultProvider(prov)
|
||||
window.dispatchEvent(new Event('models-config-changed'))
|
||||
|
|
@ -452,6 +460,8 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
parsed.model = defModels[0] || ""
|
||||
parsed.models = defModels
|
||||
parsed.knowledgeGraphModel = defConfig.knowledgeGraphModel.trim() || undefined
|
||||
parsed.meetingNotesModel = defConfig.meetingNotesModel.trim() || undefined
|
||||
parsed.trackBlockModel = defConfig.trackBlockModel.trim() || undefined
|
||||
}
|
||||
await window.ipc.invoke("workspace:writeFile", {
|
||||
path: "config/models.json",
|
||||
|
|
@ -459,7 +469,7 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
})
|
||||
setProviderConfigs(prev => ({
|
||||
...prev,
|
||||
[prov]: { apiKey: "", baseURL: defaultBaseURLs[prov] || "", models: [""], knowledgeGraphModel: "" },
|
||||
[prov]: { apiKey: "", baseURL: defaultBaseURLs[prov] || "", models: [""], knowledgeGraphModel: "", meetingNotesModel: "", trackBlockModel: "" },
|
||||
}))
|
||||
setTestState({ status: "idle" })
|
||||
window.dispatchEvent(new Event('models-config-changed'))
|
||||
|
|
@ -649,6 +659,74 @@ function ModelSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Meeting notes model */}
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Meeting notes model</span>
|
||||
{modelsLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Loading...
|
||||
</div>
|
||||
) : showModelInput ? (
|
||||
<Input
|
||||
value={activeConfig.meetingNotesModel}
|
||||
onChange={(e) => updateConfig(provider, { meetingNotesModel: e.target.value })}
|
||||
placeholder={primaryModel || "Enter model"}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
value={activeConfig.meetingNotesModel || "__same__"}
|
||||
onValueChange={(value) => updateConfig(provider, { meetingNotesModel: value === "__same__" ? "" : value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__same__">Same as assistant</SelectItem>
|
||||
{modelsForProvider.map((m) => (
|
||||
<SelectItem key={m.id} value={m.id}>
|
||||
{m.name || m.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Track block model */}
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Track block model</span>
|
||||
{modelsLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Loading...
|
||||
</div>
|
||||
) : showModelInput ? (
|
||||
<Input
|
||||
value={activeConfig.trackBlockModel}
|
||||
onChange={(e) => updateConfig(provider, { trackBlockModel: e.target.value })}
|
||||
placeholder={primaryModel || "Enter model"}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
value={activeConfig.trackBlockModel || "__same__"}
|
||||
onValueChange={(value) => updateConfig(provider, { trackBlockModel: value === "__same__" ? "" : value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__same__">Same as assistant</SelectItem>
|
||||
{modelsForProvider.map((m) => (
|
||||
<SelectItem key={m.id} value={m.id}>
|
||||
{m.name || m.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
export function getRaw(): string {
|
||||
return `---
|
||||
model: anthropic/claude-haiku-4.5
|
||||
tools:
|
||||
workspace-writeFile:
|
||||
type: builtin
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export function getRaw(): string {
|
|||
const defaultEndISO = defaultEnd.toISOString();
|
||||
|
||||
return `---
|
||||
model: anthropic/claude-sonnet-4.6
|
||||
tools:
|
||||
${toolEntries}
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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}":`,
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
model: anthropic/claude-haiku-4.5
|
||||
tools:
|
||||
workspace-readFile:
|
||||
type: builtin
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
model: anthropic/claude-haiku-4.5
|
||||
tools:
|
||||
workspace-readFile:
|
||||
type: builtin
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ export const LlmModelConfig = z.object({
|
|||
model: z.string().optional(),
|
||||
models: z.array(z.string()).optional(),
|
||||
})).optional(),
|
||||
// Deprecated: per-run model+provider supersedes these. Kept on the schema so
|
||||
// existing settings/onboarding UIs continue to compile until they're cleaned up.
|
||||
// Per-category model overrides (BYOK only — signed-in users always get
|
||||
// the curated gateway defaults). Read by helpers in core/models/defaults.ts.
|
||||
knowledgeGraphModel: z.string().optional(),
|
||||
meetingNotesModel: z.string().optional(),
|
||||
trackBlockModel: z.string().optional(),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue