mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-24 16:06:25 +02:00
Compare commits
2 commits
bdf270b7a1
...
d42fb26bcc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d42fb26bcc | ||
|
|
caf00fae0c |
25 changed files with 344 additions and 47 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 */}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,8 @@ export function TrackModal() {
|
|||
const lastRunAt = track?.lastRunAt ?? ''
|
||||
const lastRunId = track?.lastRunId ?? ''
|
||||
const lastRunSummary = track?.lastRunSummary ?? ''
|
||||
const model = track?.model ?? ''
|
||||
const provider = track?.provider ?? ''
|
||||
const scheduleSummary = useMemo(() => summarizeSchedule(schedule), [schedule])
|
||||
const triggerType: 'scheduled' | 'event' | 'manual' =
|
||||
schedule ? 'scheduled' : eventMatchCriteria ? 'event' : 'manual'
|
||||
|
|
@ -393,6 +395,12 @@ export function TrackModal() {
|
|||
<dt>Track ID</dt><dd><code>{trackId}</code></dd>
|
||||
<dt>File</dt><dd><code>{detail.filePath}</code></dd>
|
||||
<dt>Status</dt><dd>{active ? 'Active' : 'Paused'}</dd>
|
||||
{model && (<>
|
||||
<dt>Model</dt><dd><code>{model}</code></dd>
|
||||
</>)}
|
||||
{provider && (<>
|
||||
<dt>Provider</dt><dd><code>{provider}</code></dd>
|
||||
</>)}
|
||||
{lastRunAt && (<>
|
||||
<dt>Last run</dt><dd>{formatDateTime(lastRunAt)}</dd>
|
||||
</>)}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,23 @@ ${schemaYaml}
|
|||
|
||||
**Runtime-managed fields — never write these yourself:** ` + "`" + `lastRunAt` + "`" + `, ` + "`" + `lastRunId` + "`" + `, ` + "`" + `lastRunSummary` + "`" + `.
|
||||
|
||||
## Do Not Set ` + "`" + `model` + "`" + ` or ` + "`" + `provider` + "`" + ` (almost always)
|
||||
|
||||
The schema includes optional ` + "`" + `model` + "`" + ` and ` + "`" + `provider` + "`" + ` fields. **Omit them.** A user-configurable global default already picks the right model and provider for tracks; setting per-track values bypasses that and is almost always wrong.
|
||||
|
||||
The only time these belong on a track:
|
||||
|
||||
- The user **explicitly** named a model or provider for *this specific track* in their request ("use Claude Opus for this one", "force this track onto OpenAI"). Quote the user's wording back when confirming.
|
||||
|
||||
Things that are **not** reasons to set these:
|
||||
|
||||
- "Tracks should be fast" / "I want a small model" — that's a global preference, not a per-track one. Leave it; the global default exists.
|
||||
- "This track is complex" — write a clearer instruction; don't reach for a different model.
|
||||
- "Just to be safe" / "in case it matters" — this is the antipattern. Leave them out.
|
||||
- The user changed their main chat model — that has nothing to do with tracks. Leave them out.
|
||||
|
||||
When in doubt: omit both fields. Never volunteer them. Never include them in a starter template you suggest. If you find yourself adding them as a sensible default, stop — you're wrong.
|
||||
|
||||
## Choosing a trackId
|
||||
|
||||
- Kebab-case, short, descriptive: ` + "`" + `chicago-time` + "`" + `, ` + "`" + `sfo-weather` + "`" + `, ` + "`" + `hn-top5` + "`" + `, ` + "`" + `btc-usd` + "`" + `.
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -101,8 +102,15 @@ 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' });
|
||||
// Per-track model/provider overrides win when set; otherwise fall back
|
||||
// to the configured trackBlockModel default and the run-creation
|
||||
// provider default (signed-in: rowboat; BYOK: active provider).
|
||||
const model = track.track.model ?? await getTrackBlockModel();
|
||||
const agentRun = await createRun({
|
||||
agentId: 'track-run',
|
||||
model,
|
||||
...(track.track.provider ? { provider: track.track.provider } : {}),
|
||||
});
|
||||
|
||||
// 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(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ export const TrackBlockSchema = z.object({
|
|||
eventMatchCriteria: z.string().optional().describe('When set, this track participates in event-based triggering. Describe what kinds of events should consider this track for an update (e.g. "Emails about Q3 planning"). Omit to disable event triggers — the track will only run on schedule or manually.'),
|
||||
active: z.boolean().default(true).describe('Set false to pause without deleting'),
|
||||
schedule: TrackScheduleSchema.optional(),
|
||||
model: z.string().optional().describe('ADVANCED — leave unset. Per-track LLM model override (e.g. "anthropic/claude-sonnet-4.6"). Only set when the user explicitly asked for a specific model for THIS track. The global default already picks a tuned model for tracks; overriding usually makes things worse, not better.'),
|
||||
provider: z.string().optional().describe('ADVANCED — leave unset. Per-track provider name override (e.g. "openai", "anthropic"). Only set when the user explicitly asked for a specific provider for THIS track. Almost always omitted; the global default flows through correctly.'),
|
||||
lastRunAt: z.string().optional().describe('Runtime-managed — never write this yourself'),
|
||||
lastRunId: z.string().optional().describe('Runtime-managed — never write this yourself'),
|
||||
lastRunSummary: z.string().optional().describe('Runtime-managed — never write this yourself'),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue