fix(FN-2662): honor project model overrides and stabilize tests
This commit is contained in:
119
packages/core/src/__tests__/model-resolution.test.ts
Normal file
119
packages/core/src/__tests__/model-resolution.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
resolveExecutionSettingsModel,
|
||||
resolvePlanningSettingsModel,
|
||||
resolveProjectDefaultModel,
|
||||
resolveTaskExecutionModel,
|
||||
resolveTaskPlanningModel,
|
||||
resolveTaskValidatorModel,
|
||||
resolveTitleSummarizerSettingsModel,
|
||||
resolveValidatorSettingsModel,
|
||||
} from "../model-resolution.js";
|
||||
|
||||
describe("model-resolution", () => {
|
||||
it("prefers the project default override over the global default", () => {
|
||||
expect(
|
||||
resolveProjectDefaultModel({
|
||||
defaultProviderOverride: "openai",
|
||||
defaultModelIdOverride: "gpt-4o",
|
||||
defaultProvider: "anthropic",
|
||||
defaultModelId: "claude-sonnet-4-5",
|
||||
}),
|
||||
).toEqual({ provider: "openai", modelId: "gpt-4o" });
|
||||
});
|
||||
|
||||
it("uses the execution lane before the project default override", () => {
|
||||
expect(
|
||||
resolveExecutionSettingsModel({
|
||||
executionProvider: "google",
|
||||
executionModelId: "gemini-2.5-pro",
|
||||
defaultProviderOverride: "openai",
|
||||
defaultModelIdOverride: "gpt-4o",
|
||||
}),
|
||||
).toEqual({ provider: "google", modelId: "gemini-2.5-pro" });
|
||||
});
|
||||
|
||||
it("falls back from planning global to the project default override", () => {
|
||||
expect(
|
||||
resolvePlanningSettingsModel({
|
||||
defaultProviderOverride: "openai",
|
||||
defaultModelIdOverride: "gpt-4o-mini",
|
||||
}),
|
||||
).toEqual({ provider: "openai", modelId: "gpt-4o-mini" });
|
||||
});
|
||||
|
||||
it("falls back from validator global to the project default override", () => {
|
||||
expect(
|
||||
resolveValidatorSettingsModel({
|
||||
defaultProviderOverride: "anthropic",
|
||||
defaultModelIdOverride: "claude-opus-4",
|
||||
}),
|
||||
).toEqual({ provider: "anthropic", modelId: "claude-opus-4" });
|
||||
});
|
||||
|
||||
it("uses title summarizer global, then project planning, then project default override", () => {
|
||||
expect(
|
||||
resolveTitleSummarizerSettingsModel({
|
||||
titleSummarizerGlobalProvider: "openai",
|
||||
titleSummarizerGlobalModelId: "gpt-4.1",
|
||||
planningProvider: "google",
|
||||
planningModelId: "gemini-2.5-pro",
|
||||
defaultProviderOverride: "anthropic",
|
||||
defaultModelIdOverride: "claude-sonnet-4-5",
|
||||
}),
|
||||
).toEqual({ provider: "openai", modelId: "gpt-4.1" });
|
||||
|
||||
expect(
|
||||
resolveTitleSummarizerSettingsModel({
|
||||
planningProvider: "google",
|
||||
planningModelId: "gemini-2.5-pro",
|
||||
defaultProviderOverride: "anthropic",
|
||||
defaultModelIdOverride: "claude-sonnet-4-5",
|
||||
}),
|
||||
).toEqual({ provider: "google", modelId: "gemini-2.5-pro" });
|
||||
|
||||
expect(
|
||||
resolveTitleSummarizerSettingsModel({
|
||||
defaultProviderOverride: "anthropic",
|
||||
defaultModelIdOverride: "claude-sonnet-4-5",
|
||||
}),
|
||||
).toEqual({ provider: "anthropic", modelId: "claude-sonnet-4-5" });
|
||||
});
|
||||
|
||||
it("uses task overrides before settings fallbacks", () => {
|
||||
expect(
|
||||
resolveTaskExecutionModel(
|
||||
{
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
},
|
||||
{
|
||||
executionProvider: "anthropic",
|
||||
executionModelId: "claude-sonnet-4-5",
|
||||
},
|
||||
),
|
||||
).toEqual({ provider: "openai", modelId: "gpt-4o" });
|
||||
|
||||
expect(
|
||||
resolveTaskValidatorModel(
|
||||
{},
|
||||
{
|
||||
defaultProviderOverride: "anthropic",
|
||||
defaultModelIdOverride: "claude-sonnet-4-5",
|
||||
},
|
||||
),
|
||||
).toEqual({ provider: "anthropic", modelId: "claude-sonnet-4-5" });
|
||||
|
||||
expect(
|
||||
resolveTaskPlanningModel(
|
||||
{},
|
||||
{
|
||||
planningGlobalProvider: "openai",
|
||||
planningGlobalModelId: "gpt-4.1",
|
||||
defaultProviderOverride: "anthropic",
|
||||
defaultModelIdOverride: "claude-sonnet-4-5",
|
||||
},
|
||||
),
|
||||
).toEqual({ provider: "openai", modelId: "gpt-4.1" });
|
||||
});
|
||||
});
|
||||
@@ -196,6 +196,17 @@ export {
|
||||
AiServiceError,
|
||||
__resetSummarizeState,
|
||||
} from "./ai-summarize.js";
|
||||
export {
|
||||
resolveExecutionSettingsModel,
|
||||
resolvePlanningSettingsModel,
|
||||
resolveProjectDefaultModel,
|
||||
resolveTaskExecutionModel,
|
||||
resolveTaskPlanningModel,
|
||||
resolveTaskValidatorModel,
|
||||
resolveTitleSummarizerSettingsModel,
|
||||
resolveValidatorSettingsModel,
|
||||
} from "./model-resolution.js";
|
||||
export type { ResolvedModelSelection } from "./model-resolution.js";
|
||||
|
||||
// ── Memory Compaction ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
148
packages/core/src/model-resolution.ts
Normal file
148
packages/core/src/model-resolution.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import type { Settings } from "./types.js";
|
||||
|
||||
export interface ResolvedModelSelection {
|
||||
provider?: string;
|
||||
modelId?: string;
|
||||
}
|
||||
|
||||
type ModelPair =
|
||||
| ResolvedModelSelection
|
||||
| {
|
||||
provider?: string | null;
|
||||
modelId?: string | null;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
type TaskModelLike = {
|
||||
modelProvider?: string | null;
|
||||
modelId?: string | null;
|
||||
validatorModelProvider?: string | null;
|
||||
validatorModelId?: string | null;
|
||||
planningModelProvider?: string | null;
|
||||
planningModelId?: string | null;
|
||||
};
|
||||
|
||||
function hasCompleteModelPair(pair: ModelPair): pair is { provider: string; modelId: string } {
|
||||
return Boolean(pair?.provider && pair?.modelId);
|
||||
}
|
||||
|
||||
function pickFirstModelPair(...pairs: ModelPair[]): ResolvedModelSelection {
|
||||
for (const pair of pairs) {
|
||||
if (hasCompleteModelPair(pair)) {
|
||||
return { provider: pair.provider, modelId: pair.modelId };
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export function resolveProjectDefaultModel(settings?: Partial<Settings>): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.defaultProviderOverride,
|
||||
modelId: settings?.defaultModelIdOverride,
|
||||
},
|
||||
{
|
||||
provider: settings?.defaultProvider,
|
||||
modelId: settings?.defaultModelId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveExecutionSettingsModel(settings?: Partial<Settings>): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.executionProvider,
|
||||
modelId: settings?.executionModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.executionGlobalProvider,
|
||||
modelId: settings?.executionGlobalModelId,
|
||||
},
|
||||
resolveProjectDefaultModel(settings),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePlanningSettingsModel(settings?: Partial<Settings>): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.planningProvider,
|
||||
modelId: settings?.planningModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.planningGlobalProvider,
|
||||
modelId: settings?.planningGlobalModelId,
|
||||
},
|
||||
resolveProjectDefaultModel(settings),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveValidatorSettingsModel(settings?: Partial<Settings>): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.validatorProvider,
|
||||
modelId: settings?.validatorModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.validatorGlobalProvider,
|
||||
modelId: settings?.validatorGlobalModelId,
|
||||
},
|
||||
resolveProjectDefaultModel(settings),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveTitleSummarizerSettingsModel(settings?: Partial<Settings>): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.titleSummarizerProvider,
|
||||
modelId: settings?.titleSummarizerModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.titleSummarizerGlobalProvider,
|
||||
modelId: settings?.titleSummarizerGlobalModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.planningProvider,
|
||||
modelId: settings?.planningModelId,
|
||||
},
|
||||
resolveProjectDefaultModel(settings),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveTaskExecutionModel(
|
||||
task: TaskModelLike,
|
||||
settings?: Partial<Settings>,
|
||||
): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: task.modelProvider,
|
||||
modelId: task.modelId,
|
||||
},
|
||||
resolveExecutionSettingsModel(settings),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveTaskValidatorModel(
|
||||
task: TaskModelLike,
|
||||
settings?: Partial<Settings>,
|
||||
): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: task.validatorModelProvider,
|
||||
modelId: task.validatorModelId,
|
||||
},
|
||||
resolveValidatorSettingsModel(settings),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveTaskPlanningModel(
|
||||
task: TaskModelLike,
|
||||
settings?: Partial<Settings>,
|
||||
): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: task.planningModelProvider,
|
||||
modelId: task.planningModelId,
|
||||
},
|
||||
resolvePlanningSettingsModel(settings),
|
||||
);
|
||||
}
|
||||
@@ -3376,3 +3376,14 @@ export { PROMPT_KEY_CATALOG } from "./prompt-overrides.js";
|
||||
// Re-exported here so the dashboard's `@fusion/core` → types.ts alias resolves
|
||||
// client-side consumers (see packages/dashboard/vite.config.ts).
|
||||
export { getErrorMessage } from "./error-message.js";
|
||||
export {
|
||||
resolveExecutionSettingsModel,
|
||||
resolvePlanningSettingsModel,
|
||||
resolveProjectDefaultModel,
|
||||
resolveTaskExecutionModel,
|
||||
resolveTaskPlanningModel,
|
||||
resolveTaskValidatorModel,
|
||||
resolveTitleSummarizerSettingsModel,
|
||||
resolveValidatorSettingsModel,
|
||||
} from "./model-resolution.js";
|
||||
export type { ResolvedModelSelection } from "./model-resolution.js";
|
||||
|
||||
Reference in New Issue
Block a user