feat(FN-1710): add dual-scope model settings lane fields

- Add new model settings lane fields to support dual-scope configuration (global + per-project)
- Add dual-scope provider/model override fields for executor, validator, and planning models
- Add schema entries in settings-schema.ts with validation constraints
- Add backward-compat regression tests for legacy settings shapes
- Add mixed-shape tests covering global/project scopes in same config
- Update settings-reference.md documentation with new fields
This commit is contained in:
Fusion
2026-04-15 01:57:17 -07:00
committed by gsxdsm
parent b9264670e2
commit d87b2d1de5
5 changed files with 519 additions and 9 deletions

View File

@@ -45,6 +45,14 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`.
| `defaultProjectId` | `string` | `undefined` | Default project for multi-project commands. |
| `openrouterModelSync` | `boolean` | `true` | Sync OpenRouter model catalog into pickers. |
| `modelOnboardingComplete` | `boolean` | `undefined` | Whether model onboarding has been completed/dismissed. |
| `executionGlobalProvider` | `string` | `undefined` | Global baseline AI provider for task execution. Project `executionProvider` overrides this. |
| `executionGlobalModelId` | `string` | `undefined` | Global baseline AI model ID for task execution. |
| `planningGlobalProvider` | `string` | `undefined` | Global baseline AI provider for planning/triage. Project `planningProvider` overrides this. |
| `planningGlobalModelId` | `string` | `undefined` | Global baseline AI model ID for planning/triage. |
| `validatorGlobalProvider` | `string` | `undefined` | Global baseline AI provider for validator/reviewer. Project `validatorProvider` overrides this. |
| `validatorGlobalModelId` | `string` | `undefined` | Global baseline AI model ID for validator/reviewer. |
| `titleSummarizerGlobalProvider` | `string` | `undefined` | Global baseline AI provider for title summarization. Project `titleSummarizerProvider` overrides this. |
| `titleSummarizerGlobalModelId` | `string` | `undefined` | Global baseline AI model ID for title summarization. |
### Additional GlobalSettings fields
@@ -79,7 +87,11 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
| `worktreeNaming` | `"random" \| "task-id" \| "task-title"` | `"random"` | Naming mode for fresh worktree directories. |
| `taskPrefix` | `string` | `"FN"` | Prefix for generated task IDs. |
| `includeTaskIdInCommit` | `boolean` | `true` | Include task ID in commit message scope. |
| `planningProvider` | `string` | `undefined` | AI provider for triage/spec generation. |
| `defaultProviderOverride` | `string` | `undefined` | Project-level override for base default provider. Overrides global `defaultProvider`. |
| `defaultModelIdOverride` | `string` | `undefined` | Project-level override for base default model ID. |
| `executionProvider` | `string` | `undefined` | AI provider for task execution. Overrides `executionGlobalProvider`. |
| `executionModelId` | `string` | `undefined` | AI model ID for task execution. |
| `planningProvider` | `string` | `undefined` | AI provider for triage/spec generation. Overrides `planningGlobalProvider`. |
| `planningModelId` | `string` | `undefined` | Model ID for triage/spec generation. |
| `planningFallbackProvider` | `string` | `undefined` | Fallback provider for planning. |
| `planningFallbackModelId` | `string` | `undefined` | Fallback model ID for planning. |
@@ -154,25 +166,45 @@ These exist in `ProjectSettings` but are not part of `PROJECT_SETTINGS_KEYS`.
## Model Selection Hierarchy
Fusion uses a dual-scope model settings system with five lanes. Global settings provide baseline defaults, and project settings provide per-project overrides.
### Triage/specification model
1. Per-task `planningModelProvider` + `planningModelId`
2. Global/project `planningProvider` + `planningModelId`
3. Global `defaultProvider` + `defaultModelId`
4. Automatic provider/model resolution
2. Project `planningProvider` + `planningModelId`
3. Global `planningGlobalProvider` + `planningGlobalModelId`
4. Project `defaultProviderOverride` + `defaultModelIdOverride`
5. Global `defaultProvider` + `defaultModelId`
6. Automatic provider/model resolution
### Executor model
1. Per-task `modelProvider` + `modelId`
2. Global `defaultProvider` + `defaultModelId`
3. Automatic provider/model resolution
2. Project `executionProvider` + `executionModelId`
3. Global `executionGlobalProvider` + `executionGlobalModelId`
4. Project `defaultProviderOverride` + `defaultModelIdOverride`
5. Global `defaultProvider` + `defaultModelId`
6. Automatic provider/model resolution
### Reviewer model
1. Per-task `validatorModelProvider` + `validatorModelId`
2. Global/project `validatorProvider` + `validatorModelId`
3. Global `defaultProvider` + `defaultModelId`
4. Automatic provider/model resolution
2. Project `validatorProvider` + `validatorModelId`
3. Global `validatorGlobalProvider` + `validatorGlobalModelId`
4. Project `defaultProviderOverride` + `defaultModelIdOverride`
5. Global `defaultProvider` + `defaultModelId`
6. Automatic provider/model resolution
### Title summarization model
1. Project `titleSummarizerProvider` + `titleSummarizerModelId`
2. Global `titleSummarizerGlobalProvider` + `titleSummarizerGlobalModelId`
3. Project `planningProvider` + `planningModelId`
4. Project `defaultProviderOverride` + `defaultModelIdOverride`
5. Global `defaultProvider` + `defaultModelId`
6. Automatic provider/model resolution
> **Note:** Runtime fallback precedence logic is deferred to FN-1711. The hierarchy above reflects the schema contracts added in FN-1710.
---

View File

@@ -377,4 +377,128 @@ describe("GlobalSettingsStore", () => {
expect(afterInvalidate.themeMode).toBe("system");
});
});
// ── Global Lane Model Settings (FN-1710) ──────────────────────────
describe("global lane model settings", () => {
it("all *Global* lane fields default to undefined", async () => {
const settings = await store.getSettings();
expect(settings.executionGlobalProvider).toBeUndefined();
expect(settings.executionGlobalModelId).toBeUndefined();
expect(settings.planningGlobalProvider).toBeUndefined();
expect(settings.planningGlobalModelId).toBeUndefined();
expect(settings.validatorGlobalProvider).toBeUndefined();
expect(settings.validatorGlobalModelId).toBeUndefined();
expect(settings.titleSummarizerGlobalProvider).toBeUndefined();
expect(settings.titleSummarizerGlobalModelId).toBeUndefined();
});
it("persists executionGlobalProvider/executionGlobalModelId", async () => {
await store.updateSettings({
executionGlobalProvider: "anthropic",
executionGlobalModelId: "claude-sonnet-4-5",
});
const settings = await store.getSettings();
expect(settings.executionGlobalProvider).toBe("anthropic");
expect(settings.executionGlobalModelId).toBe("claude-sonnet-4-5");
// Verify persistence
const raw = JSON.parse(await readFile(join(dir, "settings.json"), "utf-8"));
expect(raw.executionGlobalProvider).toBe("anthropic");
expect(raw.executionGlobalModelId).toBe("claude-sonnet-4-5");
});
it("persists planningGlobalProvider/planningGlobalModelId", async () => {
await store.updateSettings({
planningGlobalProvider: "google",
planningGlobalModelId: "gemini-2.5-pro",
});
const settings = await store.getSettings();
expect(settings.planningGlobalProvider).toBe("google");
expect(settings.planningGlobalModelId).toBe("gemini-2.5-pro");
});
it("persists validatorGlobalProvider/validatorGlobalModelId", async () => {
await store.updateSettings({
validatorGlobalProvider: "openai",
validatorGlobalModelId: "gpt-4o",
});
const settings = await store.getSettings();
expect(settings.validatorGlobalProvider).toBe("openai");
expect(settings.validatorGlobalModelId).toBe("gpt-4o");
});
it("persists titleSummarizerGlobalProvider/titleSummarizerGlobalModelId", async () => {
await store.updateSettings({
titleSummarizerGlobalProvider: "anthropic",
titleSummarizerGlobalModelId: "claude-haiku",
});
const settings = await store.getSettings();
expect(settings.titleSummarizerGlobalProvider).toBe("anthropic");
expect(settings.titleSummarizerGlobalModelId).toBe("claude-haiku");
});
it("persists all global lane fields together", async () => {
await store.updateSettings({
executionGlobalProvider: "anthropic",
executionGlobalModelId: "claude-opus-4",
planningGlobalProvider: "google",
planningGlobalModelId: "gemini-2.5-pro",
validatorGlobalProvider: "openai",
validatorGlobalModelId: "gpt-4-turbo",
titleSummarizerGlobalProvider: "anthropic",
titleSummarizerGlobalModelId: "claude-sonnet-4-5",
});
const settings = await store.getSettings();
expect(settings.executionGlobalProvider).toBe("anthropic");
expect(settings.executionGlobalModelId).toBe("claude-opus-4");
expect(settings.planningGlobalProvider).toBe("google");
expect(settings.planningGlobalModelId).toBe("gemini-2.5-pro");
expect(settings.validatorGlobalProvider).toBe("openai");
expect(settings.validatorGlobalModelId).toBe("gpt-4-turbo");
expect(settings.titleSummarizerGlobalProvider).toBe("anthropic");
expect(settings.titleSummarizerGlobalModelId).toBe("claude-sonnet-4-5");
});
it("can clear global lane fields with null", async () => {
await store.updateSettings({
executionGlobalProvider: "anthropic",
executionGlobalModelId: "claude-sonnet-4-5",
});
// Clear them
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ executionGlobalProvider: null });
// @ts-expect-error - null is intentionally used to clear field (null-as-delete)
await store.updateSettings({ executionGlobalModelId: null });
const settings = await store.getSettings();
expect(settings.executionGlobalProvider).toBeUndefined();
expect(settings.executionGlobalModelId).toBeUndefined();
});
it("merges global lane fields without losing other fields", async () => {
await store.updateSettings({
executionGlobalProvider: "anthropic",
executionGlobalModelId: "claude-sonnet-4-5",
});
await store.updateSettings({
planningGlobalProvider: "google",
planningGlobalModelId: "gemini-2.5-pro",
});
const settings = await store.getSettings();
// Both should be present
expect(settings.executionGlobalProvider).toBe("anthropic");
expect(settings.executionGlobalModelId).toBe("claude-sonnet-4-5");
expect(settings.planningGlobalProvider).toBe("google");
expect(settings.planningGlobalModelId).toBe("gemini-2.5-pro");
});
});
});

View File

@@ -29,6 +29,15 @@ export const DEFAULT_GLOBAL_SETTINGS = {
favoriteModels: undefined,
openrouterModelSync: true,
modelOnboardingComplete: undefined,
// Global baseline lanes for per-role model selection
executionGlobalProvider: undefined,
executionGlobalModelId: undefined,
planningGlobalProvider: undefined,
planningGlobalModelId: undefined,
validatorGlobalProvider: undefined,
validatorGlobalModelId: undefined,
titleSummarizerGlobalProvider: undefined,
titleSummarizerGlobalModelId: undefined,
} satisfies CompleteSettings<GlobalSettings>;
/** Default values for project-level settings. */
@@ -56,6 +65,11 @@ export const DEFAULT_PROJECT_SETTINGS = {
planningModelId: undefined,
planningFallbackProvider: undefined,
planningFallbackModelId: undefined,
// Project-level default override and execution lane
defaultProviderOverride: undefined,
defaultModelIdOverride: undefined,
executionProvider: undefined,
executionModelId: undefined,
validatorProvider: undefined,
validatorModelId: undefined,
validatorFallbackProvider: undefined,

View File

@@ -705,6 +705,292 @@ describe("TaskStore", () => {
});
});
// ── Dual-Scope Lane Model Settings (FN-1710) ─────────────────────
describe("dual-scope lane model settings", () => {
// Legacy backward compatibility tests
it("legacy: project config with only planningProvider/planningModelId round-trips unchanged", async () => {
await store.updateSettings({
planningProvider: "anthropic",
planningModelId: "claude-sonnet-4-5",
});
const settings = await store.getSettings();
expect(settings.planningProvider).toBe("anthropic");
expect(settings.planningModelId).toBe("claude-sonnet-4-5");
// Verify it's persisted correctly
const configRaw = await readFile(join(rootDir, ".fusion", "config.json"), "utf-8");
const config = JSON.parse(configRaw);
expect(config.settings.planningProvider).toBe("anthropic");
expect(config.settings.planningModelId).toBe("claude-sonnet-4-5");
});
it("legacy: project config with only validatorProvider/validatorModelId round-trips unchanged", async () => {
await store.updateSettings({
validatorProvider: "openai",
validatorModelId: "gpt-4o",
});
const settings = await store.getSettings();
expect(settings.validatorProvider).toBe("openai");
expect(settings.validatorModelId).toBe("gpt-4o");
const configRaw = await readFile(join(rootDir, ".fusion", "config.json"), "utf-8");
const config = JSON.parse(configRaw);
expect(config.settings.validatorProvider).toBe("openai");
expect(config.settings.validatorModelId).toBe("gpt-4o");
});
it("legacy: project config with only titleSummarizerProvider/titleSummarizerModelId round-trips unchanged", async () => {
await store.updateSettings({
titleSummarizerProvider: "google",
titleSummarizerModelId: "gemini-2.5-pro",
});
const settings = await store.getSettings();
expect(settings.titleSummarizerProvider).toBe("google");
expect(settings.titleSummarizerModelId).toBe("gemini-2.5-pro");
const configRaw = await readFile(join(rootDir, ".fusion", "config.json"), "utf-8");
const config = JSON.parse(configRaw);
expect(config.settings.titleSummarizerProvider).toBe("google");
expect(config.settings.titleSummarizerModelId).toBe("gemini-2.5-pro");
});
it("legacy: partial provider without modelId behaves correctly", async () => {
// Set provider only without modelId (partial legacy pair)
await store.updateSettings({
planningProvider: "anthropic",
// No planningModelId
});
const settings = await store.getSettings();
expect(settings.planningProvider).toBe("anthropic");
expect(settings.planningModelId).toBeUndefined();
});
// New default override fields
it("persists defaultProviderOverride/defaultModelIdOverride via updateSettings", async () => {
await store.updateSettings({
defaultProviderOverride: "openai",
defaultModelIdOverride: "gpt-4o-mini",
});
const settings = await store.getSettings();
expect(settings.defaultProviderOverride).toBe("openai");
expect(settings.defaultModelIdOverride).toBe("gpt-4o-mini");
});
it("defaultProviderOverride/defaultModelIdOverride appear in project scope", async () => {
await store.updateSettings({
defaultProviderOverride: "anthropic",
defaultModelIdOverride: "claude-3-5-sonnet",
});
const { project } = await store.getSettingsByScope();
expect(project.defaultProviderOverride).toBe("anthropic");
expect(project.defaultModelIdOverride).toBe("claude-3-5-sonnet");
});
it("defaultProviderOverride/defaultModelIdOverride default to undefined", async () => {
const settings = await store.getSettings();
expect(settings.defaultProviderOverride).toBeUndefined();
expect(settings.defaultModelIdOverride).toBeUndefined();
});
// New execution lane fields
it("persists executionProvider/executionModelId via updateSettings", async () => {
await store.updateSettings({
executionProvider: "anthropic",
executionModelId: "claude-opus-4",
});
const settings = await store.getSettings();
expect(settings.executionProvider).toBe("anthropic");
expect(settings.executionModelId).toBe("claude-opus-4");
});
it("executionProvider/executionModelId appear in project scope", async () => {
await store.updateSettings({
executionProvider: "openai",
executionModelId: "gpt-4-turbo",
});
const { project } = await store.getSettingsByScope();
expect(project.executionProvider).toBe("openai");
expect(project.executionModelId).toBe("gpt-4-turbo");
});
it("executionProvider/executionModelId default to undefined", async () => {
const settings = await store.getSettings();
expect(settings.executionProvider).toBeUndefined();
expect(settings.executionModelId).toBeUndefined();
});
// Global lane fields via updateGlobalSettings
it("persists executionGlobalProvider/executionGlobalModelId via updateGlobalSettings", async () => {
await store.updateGlobalSettings({
executionGlobalProvider: "anthropic",
executionGlobalModelId: "claude-sonnet-4-5",
});
const settings = await store.getSettings();
expect(settings.executionGlobalProvider).toBe("anthropic");
expect(settings.executionGlobalModelId).toBe("claude-sonnet-4-5");
});
it("persists planningGlobalProvider/planningGlobalModelId via updateGlobalSettings", async () => {
await store.updateGlobalSettings({
planningGlobalProvider: "google",
planningGlobalModelId: "gemini-2.5-pro",
});
const settings = await store.getSettings();
expect(settings.planningGlobalProvider).toBe("google");
expect(settings.planningGlobalModelId).toBe("gemini-2.5-pro");
});
it("persists validatorGlobalProvider/validatorGlobalModelId via updateGlobalSettings", async () => {
await store.updateGlobalSettings({
validatorGlobalProvider: "openai",
validatorGlobalModelId: "gpt-4o",
});
const settings = await store.getSettings();
expect(settings.validatorGlobalProvider).toBe("openai");
expect(settings.validatorGlobalModelId).toBe("gpt-4o");
});
it("persists titleSummarizerGlobalProvider/titleSummarizerGlobalModelId via updateGlobalSettings", async () => {
await store.updateGlobalSettings({
titleSummarizerGlobalProvider: "anthropic",
titleSummarizerGlobalModelId: "claude-haiku",
});
const settings = await store.getSettings();
expect(settings.titleSummarizerGlobalProvider).toBe("anthropic");
expect(settings.titleSummarizerGlobalModelId).toBe("claude-haiku");
});
it("all *Global* lane fields default to undefined", async () => {
const settings = await store.getSettings();
expect(settings.executionGlobalProvider).toBeUndefined();
expect(settings.executionGlobalModelId).toBeUndefined();
expect(settings.planningGlobalProvider).toBeUndefined();
expect(settings.planningGlobalModelId).toBeUndefined();
expect(settings.validatorGlobalProvider).toBeUndefined();
expect(settings.validatorGlobalModelId).toBeUndefined();
expect(settings.titleSummarizerGlobalProvider).toBeUndefined();
expect(settings.titleSummarizerGlobalModelId).toBeUndefined();
});
// Mixed shape compatibility tests
it("mixed shape: project planningProvider + global planningGlobalProvider is stable", async () => {
// Set global baseline
await store.updateGlobalSettings({
planningGlobalProvider: "anthropic",
planningGlobalModelId: "claude-sonnet-4-5",
});
// Set project override
await store.updateSettings({
planningProvider: "openai",
planningModelId: "gpt-4o",
});
// Both should be readable with no crashes
const settings = await store.getSettings();
expect(settings.planningGlobalProvider).toBe("anthropic");
expect(settings.planningGlobalModelId).toBe("claude-sonnet-4-5");
expect(settings.planningProvider).toBe("openai");
expect(settings.planningModelId).toBe("gpt-4o");
});
it("mixed shape: project validatorProvider + global validatorGlobalProvider is stable", async () => {
await store.updateGlobalSettings({
validatorGlobalProvider: "google",
validatorGlobalModelId: "gemini-2.5-pro",
});
await store.updateSettings({
validatorProvider: "anthropic",
validatorModelId: "claude-opus-4",
});
const settings = await store.getSettings();
expect(settings.validatorGlobalProvider).toBe("google");
expect(settings.validatorGlobalModelId).toBe("gemini-2.5-pro");
expect(settings.validatorProvider).toBe("anthropic");
expect(settings.validatorModelId).toBe("claude-opus-4");
});
it("mixed shape: project titleSummarizerProvider + global titleSummarizerGlobalProvider is stable", async () => {
await store.updateGlobalSettings({
titleSummarizerGlobalProvider: "openai",
titleSummarizerGlobalModelId: "gpt-4o-mini",
});
await store.updateSettings({
titleSummarizerProvider: "anthropic",
titleSummarizerModelId: "claude-haiku",
});
const settings = await store.getSettings();
expect(settings.titleSummarizerGlobalProvider).toBe("openai");
expect(settings.titleSummarizerGlobalModelId).toBe("gpt-4o-mini");
expect(settings.titleSummarizerProvider).toBe("anthropic");
expect(settings.titleSummarizerModelId).toBe("claude-haiku");
});
// Global-only key filtering tests
it("updateSettings does not persist *Global* keys to project config", async () => {
// Attempt to set global keys through updateSettings (should be filtered)
await store.updateSettings({
executionGlobalProvider: "anthropic",
executionGlobalModelId: "claude-sonnet-4-5",
planningGlobalProvider: "openai",
planningGlobalModelId: "gpt-4o",
} as any);
// The global keys should be filtered out and not appear in merged settings
const settings = await store.getSettings();
expect(settings.executionGlobalProvider).toBeUndefined();
expect(settings.executionGlobalModelId).toBeUndefined();
expect(settings.planningGlobalProvider).toBeUndefined();
expect(settings.planningGlobalModelId).toBeUndefined();
// Verify they are NOT in the project config
const configRaw = await readFile(join(rootDir, ".fusion", "config.json"), "utf-8");
const config = JSON.parse(configRaw);
expect((config.settings as any).executionGlobalProvider).toBeUndefined();
expect((config.settings as any).planningGlobalProvider).toBeUndefined();
});
it("all global lane fields appear in global scope", async () => {
await store.updateGlobalSettings({
executionGlobalProvider: "anthropic",
executionGlobalModelId: "claude-sonnet-4-5",
planningGlobalProvider: "google",
planningGlobalModelId: "gemini-2.5-pro",
validatorGlobalProvider: "openai",
validatorGlobalModelId: "gpt-4o",
titleSummarizerGlobalProvider: "anthropic",
titleSummarizerGlobalModelId: "claude-haiku",
});
const { global } = await store.getSettingsByScope();
expect(global.executionGlobalProvider).toBe("anthropic");
expect(global.executionGlobalModelId).toBe("claude-sonnet-4-5");
expect(global.planningGlobalProvider).toBe("google");
expect(global.planningGlobalModelId).toBe("gemini-2.5-pro");
expect(global.validatorGlobalProvider).toBe("openai");
expect(global.validatorGlobalModelId).toBe("gpt-4o");
expect(global.titleSummarizerGlobalProvider).toBe("anthropic");
expect(global.titleSummarizerGlobalModelId).toBe("claude-haiku");
});
});
// ── Global/Project Settings Merging ─────────────────────────────
describe("global/project settings merging", () => {

View File

@@ -899,6 +899,38 @@ export interface GlobalSettings {
* false/undefined, the dashboard will auto-open the onboarding modal.
* Also set to true when the user explicitly dismisses onboarding. */
modelOnboardingComplete?: boolean;
/** Global baseline AI model provider for task execution (executor agent).
* This is the global lane that project-level `executionProvider` can override.
* Must be set together with `executionGlobalModelId`. Falls back to
* `defaultProvider`/`defaultModelId` when undefined. */
executionGlobalProvider?: string;
/** Global baseline AI model ID for task execution.
* Must be set together with `executionGlobalProvider`. */
executionGlobalModelId?: string;
/** Global baseline AI model provider for planning/triage (specification) agent.
* This is the global lane that project-level `planningProvider` can override.
* Must be set together with `planningGlobalModelId`. Falls back to
* `defaultProvider`/`defaultModelId` when undefined. */
planningGlobalProvider?: string;
/** Global baseline AI model ID for planning/triage.
* Must be set together with `planningGlobalProvider`. */
planningGlobalModelId?: string;
/** Global baseline AI model provider for validator/reviewer agent.
* This is the global lane that project-level `validatorProvider` can override.
* Must be set together with `validatorGlobalModelId`. Falls back to
* `defaultProvider`/`defaultModelId` when undefined. */
validatorGlobalProvider?: string;
/** Global baseline AI model ID for validator/reviewer.
* Must be set together with `validatorGlobalProvider`. */
validatorGlobalModelId?: string;
/** Global baseline AI model provider for title summarization.
* This is the global lane that project-level `titleSummarizerProvider` can override.
* Must be set together with `titleSummarizerGlobalModelId`. Falls back to
* `defaultProvider`/`defaultModelId` when undefined. */
titleSummarizerGlobalProvider?: string;
/** Global baseline AI model ID for title summarization.
* Must be set together with `titleSummarizerGlobalProvider`. */
titleSummarizerGlobalModelId?: string;
}
/**
@@ -993,6 +1025,28 @@ export interface ProjectSettings {
/** Fallback model ID for planning/triage. When unset, falls back to the
* global fallback model. Must be set together with `planningFallbackProvider`. */
planningFallbackModelId?: string;
/** Project-level override for the base default AI model provider.
* When set, this overrides the global `defaultProvider`/`defaultModelId` baseline
* for all lanes that don't have their own explicit project override.
* Must be set together with `defaultModelIdOverride`.
* NOTE: This is a schema/persistence contract only. Runtime fallback precedence
* logic is deferred to FN-1711. */
defaultProviderOverride?: string;
/** Project-level override for the base default AI model ID.
* Must be set together with `defaultProviderOverride`. */
defaultModelIdOverride?: string;
/** Project-level AI model provider for task execution (executor agent).
* This is the execution lane that overrides the global `executionGlobalProvider`.
* Must be set together with `executionModelId`. Falls back to
* `executionGlobalProvider`/`executionGlobalModelId` or
* `defaultProviderOverride`/`defaultModelIdOverride` or
* `defaultProvider`/`defaultModelId` when undefined.
* NOTE: This is a schema/persistence contract only. Runtime fallback precedence
* logic is deferred to FN-1711. */
executionProvider?: string;
/** Project-level AI model ID for task execution.
* Must be set together with `executionProvider`. */
executionModelId?: string;
/** AI model provider for validator/reviewer agent.
* Must be set together with `validatorModelId`. When both are undefined,
* falls back to `defaultProvider`/`defaultModelId`. */