From d42fb26bcc5ba4dde1de49c4df7054ebb9cd5575 Mon Sep 17 00:00:00 2001
From: Ramnique Singh <30795890+ramnique@users.noreply.github.com>
Date: Fri, 24 Apr 2026 16:58:18 +0530
Subject: [PATCH] allow per-track model + provider overrides
Track block YAML gains optional `model` and `provider` fields. When set,
the track runner passes them through to `createRun` so this specific
track runs on the chosen model/provider; when unset the global default
flows through (`getTrackBlockModel()` + the resolved provider).
The track skill picks up the new fields automatically via the embedded
`z.toJSONSchema(TrackBlockSchema)` and adds an explicit "Do Not Set"
section: copilot leaves them omitted unless the user named a specific
model or provider for the track. Common bad reasons ("might be faster",
"in case it matters", complex instruction) are called out so the
defaults stay the path of least resistance.
Track modal Details tab shows the values when set, in the same
conditional `
/` style as the lastRun fields.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../renderer/src/components/track-modal.tsx | 8 ++++++++
.../assistant/skills/tracks/skill.ts | 17 +++++++++++++++++
.../packages/core/src/knowledge/track/runner.ts | 11 +++++++++--
apps/x/packages/shared/src/track-block.ts | 2 ++
4 files changed, 36 insertions(+), 2 deletions(-)
diff --git a/apps/x/apps/renderer/src/components/track-modal.tsx b/apps/x/apps/renderer/src/components/track-modal.tsx
index 8e261977..a4c0b512 100644
--- a/apps/x/apps/renderer/src/components/track-modal.tsx
+++ b/apps/x/apps/renderer/src/components/track-modal.tsx
@@ -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() {
Track ID{trackId}
File{detail.filePath}
Status{active ? 'Active' : 'Paused'}
+ {model && (<>
+ Model{model}
+ >)}
+ {provider && (<>
+ Provider{provider}
+ >)}
{lastRunAt && (<>
Last run{formatDateTime(lastRunAt)}
>)}
diff --git a/apps/x/packages/core/src/application/assistant/skills/tracks/skill.ts b/apps/x/packages/core/src/application/assistant/skills/tracks/skill.ts
index ff345acf..17521806 100644
--- a/apps/x/packages/core/src/application/assistant/skills/tracks/skill.ts
+++ b/apps/x/packages/core/src/application/assistant/skills/tracks/skill.ts
@@ -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` + "`" + `.
diff --git a/apps/x/packages/core/src/knowledge/track/runner.ts b/apps/x/packages/core/src/knowledge/track/runner.ts
index 35f7e7ac..1eec3da1 100644
--- a/apps/x/packages/core/src/knowledge/track/runner.ts
+++ b/apps/x/packages/core/src/knowledge/track/runner.ts
@@ -102,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', model: await getTrackBlockModel() });
+ // 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.
diff --git a/apps/x/packages/shared/src/track-block.ts b/apps/x/packages/shared/src/track-block.ts
index c9e738b7..6d9ce3af 100644
--- a/apps/x/packages/shared/src/track-block.ts
+++ b/apps/x/packages/shared/src/track-block.ts
@@ -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'),