FN-7901: persist thinkingLevel for insight model selection
Adds a persisted Thinking Level (reasoning-effort) selector to manual insight generation, threading the selection through the dashboard API, insight run metadata, and retries. - Add inline Thinking Level selector to the InsightsView model-config popover, persisted to localStorage (fusion-insight-thinking) - Thread thinkingLevel through triggerInsightRun (legacy API client) and useInsights.runInsights - Validate and store thinkingLevel in insight run inputMetadata.metadata on the POST /insights/run route; resolve it via resolvePlanningThinkingLevel for the actual generation call - Recover and reapply the original run's thinkingLevel on retry (retryInsightRunLifecycle) so retries reuse the same reasoning-effort setting - Export resolvePlanningThinkingLevel from @fusion/engine - Document the new Thinking Level selector in docs/dashboard-guide.md - Add a minor changeset for @runfusion/fusion Files changed: .changeset/fn-7901-insight-thinking-level.md | 7 ++ docs/dashboard-guide.md | 1 + .../app/__tests__/insight-model-selector.test.tsx | 41 ++++++++++- packages/dashboard/app/api/legacy.ts | 2 + packages/dashboard/app/components/InsightsView.tsx | 24 +++++- .../app/hooks/__tests__/useInsights.test.ts | 36 ++++++++- packages/dashboard/app/hooks/useInsights.ts | 6 +- .../src/__tests__/insights-routes.test.ts | 86 ++++++++++++++++++++++ packages/dashboard/src/insights-routes.ts | 36 ++++++++- packages/engine/src/index.ts | 1 + 10 files changed, 227 insertions(+), 13 deletions(-) Fusion-Task-Id: FN-7901 Fusion-Task-Lineage: a6249526-e97d-403e-b853-e497d16f425b Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7901-insight-thinking-level.md
Normal file
7
.changeset/fn-7901-insight-thinking-level.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Add a persisted Thinking Level selector for manual insight generation.
|
||||
category: feature
|
||||
dev: Threads insight run thinkingLevel through dashboard API metadata and retry generation.
|
||||
@@ -1107,6 +1107,7 @@ Navigation:
|
||||
Features:
|
||||
- Category-based insight browser with run metadata and status indicators
|
||||
- Manual insight generation plus refresh actions for latest insight runs
|
||||
- The model gear beside **Generate Insights** opens a model picker with an inline **Thinking Level** selector. Both the model override and reasoning-effort choice persist in the browser, and each insight run records the selected reasoning effort so retries reuse the same setting.
|
||||
- Dismiss/archive/unarchive insight records as they age
|
||||
- Create triage tasks from selected insights directly from the view
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||
import * as jestDomMatchers from "@testing-library/jest-dom/matchers";
|
||||
import { InsightsView } from "../components/InsightsView";
|
||||
|
||||
const mockRunInsights = vi.hoisted(() => vi.fn());
|
||||
|
||||
// Register jest-dom matchers (setup files not running in this environment)
|
||||
expect.extend(jestDomMatchers);
|
||||
|
||||
@@ -50,6 +52,7 @@ beforeAll(() => {
|
||||
// Mock useInsights hook
|
||||
vi.mock("../hooks/useInsights", () => ({
|
||||
useInsights: () => ({
|
||||
|
||||
sections: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
@@ -57,7 +60,7 @@ vi.mock("../hooks/useInsights", () => ({
|
||||
isRunInFlight: false,
|
||||
runError: null,
|
||||
refresh: vi.fn(),
|
||||
runInsights: vi.fn(),
|
||||
runInsights: mockRunInsights,
|
||||
dismiss: vi.fn(),
|
||||
createTask: vi.fn(),
|
||||
archive: vi.fn(),
|
||||
@@ -76,10 +79,17 @@ vi.mock("../hooks/useInsights", () => ({
|
||||
|
||||
// Mock CustomModelDropdown since it has complex portal behavior
|
||||
vi.mock("../components/CustomModelDropdown", () => ({
|
||||
CustomModelDropdown: ({ value, onChange, placeholder }: any) => (
|
||||
<div data-testid="model-dropdown">
|
||||
CustomModelDropdown: ({ value, onChange, placeholder, thinkingLevel, onThinkingLevelChange, showThinkingLevel, disabled }: any) => (
|
||||
<div data-testid="model-dropdown" data-disabled={disabled ? "true" : "false"}>
|
||||
<span data-testid="model-value">{value || placeholder}</span>
|
||||
<button data-testid="model-change" onClick={() => onChange("openai/gpt-4o")} />
|
||||
{showThinkingLevel && (
|
||||
<div data-testid="thinking-control">
|
||||
<span data-testid="thinking-value">{thinkingLevel || "default"}</span>
|
||||
<button data-testid="thinking-change" onClick={() => onThinkingLevelChange("high")} />
|
||||
<button data-testid="thinking-clear" onClick={() => onThinkingLevelChange("")} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -89,6 +99,7 @@ const mockAddToast = vi.fn();
|
||||
describe("Insight model selector", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
mockRunInsights.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -143,4 +154,28 @@ describe("Insight model selector", () => {
|
||||
screen.getByTestId("toggle-model-config").querySelector(".insights-model-indicator"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("persists selected thinking level to localStorage", () => {
|
||||
render(<InsightsView addToast={mockAddToast} />);
|
||||
fireEvent.click(screen.getByTestId("toggle-model-config"));
|
||||
expect(screen.getByTestId("thinking-control")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId("thinking-change"));
|
||||
expect(localStorage.getItem("fusion-insight-thinking")).toBe("high");
|
||||
expect(screen.getByTestId("thinking-value")).toHaveTextContent("high");
|
||||
|
||||
fireEvent.click(screen.getByTestId("thinking-clear"));
|
||||
expect(localStorage.getItem("fusion-insight-thinking")).toBeNull();
|
||||
});
|
||||
|
||||
it("forwards persisted thinking level when generating insights", () => {
|
||||
render(<InsightsView addToast={mockAddToast} />);
|
||||
fireEvent.click(screen.getByTestId("toggle-model-config"));
|
||||
fireEvent.click(screen.getByTestId("model-change"));
|
||||
fireEvent.click(screen.getByTestId("thinking-change"));
|
||||
|
||||
fireEvent.click(screen.getByTestId("run-insights"));
|
||||
|
||||
expect(mockRunInsights).toHaveBeenCalledWith("openai", "gpt-4o", "high");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11162,10 +11162,12 @@ export function triggerInsightRun(
|
||||
projectId?: string,
|
||||
modelProvider?: string,
|
||||
modelId?: string,
|
||||
thinkingLevel?: string,
|
||||
): Promise<InsightRun> {
|
||||
const body: Record<string, unknown> = { trigger, inputMetadata };
|
||||
if (modelProvider) body.modelProvider = modelProvider;
|
||||
if (modelId) body.modelId = modelId;
|
||||
if (thinkingLevel) body.thinkingLevel = thinkingLevel;
|
||||
return api<InsightRun>(withProjectId("/insights/run", projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
|
||||
@@ -93,6 +93,9 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask, model
|
||||
const [selectedModel, setSelectedModel] = useState<string>(
|
||||
() => localStorage.getItem("fusion-insight-model") ?? ""
|
||||
);
|
||||
const [selectedThinking, setSelectedThinking] = useState<string>(
|
||||
() => localStorage.getItem("fusion-insight-thinking") ?? ""
|
||||
);
|
||||
|
||||
// Fetch models internally if not provided via prop
|
||||
const [fetchedModels, setFetchedModels] = useState<ModelInfo[]>([]);
|
||||
@@ -166,6 +169,15 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask, model
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleThinkingChange = useCallback((value: string) => {
|
||||
setSelectedThinking(value);
|
||||
if (value) {
|
||||
localStorage.setItem("fusion-insight-thinking", value);
|
||||
} else {
|
||||
localStorage.removeItem("fusion-insight-thinking");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const populatedSections = useMemo(
|
||||
() => sections.filter((section) => section.items.length > 0),
|
||||
[sections],
|
||||
@@ -234,7 +246,7 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask, model
|
||||
}
|
||||
}
|
||||
|
||||
await runInsights(modelProvider, modelId);
|
||||
await runInsights(modelProvider, modelId, selectedThinking || undefined);
|
||||
setStatusMessage(t("insights.generationStarted", "Insight generation started"));
|
||||
setStatusType("success");
|
||||
addToast(t("insights.generationStarted", "Insight generation started"), "success");
|
||||
@@ -250,7 +262,7 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask, model
|
||||
setStatusType("error");
|
||||
addToast(message, "error");
|
||||
}
|
||||
}, [runInsights, addToast, selectedModel, t]);
|
||||
}, [runInsights, addToast, selectedModel, selectedThinking, t]);
|
||||
|
||||
const handleDismiss = useCallback(
|
||||
async (id: string, title: string) => {
|
||||
@@ -577,6 +589,10 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask, model
|
||||
|
||||
{showModelConfig && (
|
||||
<div className="insights-model-config" data-testid="model-config">
|
||||
{/*
|
||||
FNXC:Insights-ThinkingLevel 2026-07-12-19:24:
|
||||
The insight model-config popover must expose the shared inline reasoning-effort selector and persist the operator's choice next to the model override so Generate Insights can send a real per-run selection.
|
||||
*/}
|
||||
<label htmlFor="insight-model-select" className="insights-model-label">
|
||||
{t("insights.model", "Model")}
|
||||
</label>
|
||||
@@ -592,6 +608,10 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask, model
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleFavorite={handleToggleProviderFavorite}
|
||||
onToggleModelFavorite={handleToggleModelFavorite}
|
||||
showThinkingLevel
|
||||
thinkingLevel={selectedThinking}
|
||||
onThinkingLevelChange={handleThinkingChange}
|
||||
defaultThinkingLevel="off"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -379,13 +379,47 @@ describe("useInsights", () => {
|
||||
await result.current.runInsights();
|
||||
});
|
||||
|
||||
expect(mockTriggerInsightRun).toHaveBeenCalledWith("manual", undefined, "project-1", undefined, undefined);
|
||||
expect(mockTriggerInsightRun).toHaveBeenCalledWith("manual", undefined, "project-1", undefined, undefined, undefined);
|
||||
expect(result.current.latestRun?.status).toBe("completed");
|
||||
expect(result.current.isRunInFlight).toBe(false);
|
||||
expect(mockFetchInsights.mock.calls.length).toBeGreaterThanOrEqual(2);
|
||||
expect(mockFetchInsightRuns.mock.calls.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("should forward a selected thinking level into manual insight runs", async () => {
|
||||
const runningRun = {
|
||||
id: "RUN-THINKING",
|
||||
projectId: "project-1",
|
||||
trigger: "manual" as const,
|
||||
status: "running" as const,
|
||||
summary: null,
|
||||
error: null,
|
||||
insightsCreated: 0,
|
||||
insightsUpdated: 0,
|
||||
inputMetadata: {},
|
||||
outputMetadata: {},
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
startedAt: "2024-01-01T00:00:10Z",
|
||||
completedAt: null,
|
||||
};
|
||||
|
||||
mockFetchInsights.mockResolvedValue({ insights: [], count: 0 });
|
||||
mockFetchInsightRuns.mockResolvedValue({ runs: [] });
|
||||
mockTriggerInsightRun.mockResolvedValue(runningRun);
|
||||
|
||||
const { result } = renderHook(() => useInsights("project-1"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.runInsights("anthropic", "claude-sonnet-4-5", "high");
|
||||
});
|
||||
|
||||
expect(mockTriggerInsightRun).toHaveBeenCalledWith("manual", undefined, "project-1", "anthropic", "claude-sonnet-4-5", "high");
|
||||
});
|
||||
|
||||
it("should surface failed run errors from triggerInsightRun response", async () => {
|
||||
const failedRun = {
|
||||
id: "RUN-1",
|
||||
|
||||
@@ -103,7 +103,7 @@ export interface UseInsightsResult {
|
||||
|
||||
// Actions
|
||||
refresh: () => Promise<void>;
|
||||
runInsights: (modelProvider?: string, modelId?: string) => Promise<void>;
|
||||
runInsights: (modelProvider?: string, modelId?: string, thinkingLevel?: string) => Promise<void>;
|
||||
dismiss: (id: string) => Promise<void>;
|
||||
createTask: (id: string) => Promise<{ title: string; description: string } | null>;
|
||||
archive: (id: string) => Promise<void>;
|
||||
@@ -216,12 +216,12 @@ export function useInsights(projectId?: string): UseInsightsResult {
|
||||
}, []);
|
||||
|
||||
// Run insights generation
|
||||
const runInsights = useCallback(async (modelProvider?: string, modelId?: string) => {
|
||||
const runInsights = useCallback(async (modelProvider?: string, modelId?: string, thinkingLevel?: string) => {
|
||||
setIsRunInFlight(true);
|
||||
setRunError(null);
|
||||
|
||||
try {
|
||||
const run = await triggerInsightRun("manual", undefined, projectId, modelProvider, modelId);
|
||||
const run = await triggerInsightRun("manual", undefined, projectId, modelProvider, modelId, thinkingLevel);
|
||||
setLatestRun(run);
|
||||
|
||||
if (run.status === "completed") {
|
||||
|
||||
@@ -15,6 +15,7 @@ const piMocks = vi.hoisted(() => ({
|
||||
createFnAgent: vi.fn(),
|
||||
promptWithFallback: vi.fn(),
|
||||
resolveMcpServersForStore: vi.fn(),
|
||||
resolvePlanningThinkingLevel: vi.fn(),
|
||||
}));
|
||||
|
||||
const resolverMocks = vi.hoisted(() => ({
|
||||
@@ -56,6 +57,7 @@ vi.mock("@fusion/engine", () => ({
|
||||
createFnAgent: piMocks.createFnAgent,
|
||||
promptWithFallback: piMocks.promptWithFallback,
|
||||
resolveMcpServersForStore: piMocks.resolveMcpServersForStore,
|
||||
resolvePlanningThinkingLevel: piMocks.resolvePlanningThinkingLevel,
|
||||
}));
|
||||
|
||||
/*
|
||||
@@ -206,6 +208,7 @@ describe("Insights routes", () => {
|
||||
}));
|
||||
piMocks.promptWithFallback.mockResolvedValue(undefined);
|
||||
piMocks.resolveMcpServersForStore.mockResolvedValue({ servers: [] });
|
||||
piMocks.resolvePlanningThinkingLevel.mockImplementation((_settings: unknown, thinkingLevel?: string) => thinkingLevel);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -565,6 +568,56 @@ describe("Insights routes", () => {
|
||||
expect(persisted?.inputMetadata).toEqual({ source: "route-test" });
|
||||
});
|
||||
|
||||
it("POST /api/insights/run accepts and stores a valid thinkingLevel", async () => {
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/insights/run",
|
||||
JSON.stringify({ trigger: "manual", thinkingLevel: "high" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
const run = res.body as { id: string; inputMetadata: { metadata?: Record<string, unknown> } };
|
||||
expect(run.inputMetadata.metadata).toMatchObject({ thinkingLevel: "high" });
|
||||
expect(storeA.getInsightStore().getRun(run.id)?.inputMetadata?.metadata).toMatchObject({ thinkingLevel: "high" });
|
||||
expect(piMocks.resolvePlanningThinkingLevel).toHaveBeenCalledWith(expect.any(Object), "high");
|
||||
expect(piMocks.createFnAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
defaultThinkingLevel: "high",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("POST /api/insights/run rejects an invalid thinkingLevel", async () => {
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/insights/run",
|
||||
JSON.stringify({ trigger: "manual", thinkingLevel: "turbo" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect((res.body as { error: string }).error).toContain("Invalid thinkingLevel");
|
||||
expect(storeA.getInsightStore().listRuns({})).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("POST /api/insights/run omits thinkingLevel metadata when inheriting defaults", async () => {
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/insights/run",
|
||||
JSON.stringify({ trigger: "manual" }),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
const run = res.body as { inputMetadata: { metadata?: Record<string, unknown> } };
|
||||
expect(run.inputMetadata.metadata).toBeUndefined();
|
||||
expect(piMocks.resolvePlanningThinkingLevel).toHaveBeenCalledWith(expect.any(Object), undefined);
|
||||
});
|
||||
|
||||
it("POST /api/insights/run marks run failed when AI execution throws", async () => {
|
||||
piMocks.promptWithFallback.mockRejectedValue(new Error("AI blew up"));
|
||||
|
||||
@@ -696,6 +749,39 @@ describe("Insights routes", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("POST /api/insights/runs/:id/retry preserves the original run's thinkingLevel", async () => {
|
||||
piMocks.promptWithFallback.mockRejectedValue(new Error("HTTP 503"));
|
||||
const failedRes = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/insights/run",
|
||||
JSON.stringify({
|
||||
trigger: "manual",
|
||||
thinkingLevel: "xhigh",
|
||||
}),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
expect(failedRes.status).toBe(201);
|
||||
const failedRun = failedRes.body as { id: string; inputMetadata: { metadata?: Record<string, unknown> } };
|
||||
expect(failedRun.inputMetadata.metadata).toMatchObject({ thinkingLevel: "xhigh" });
|
||||
|
||||
piMocks.promptWithFallback.mockResolvedValue(undefined);
|
||||
piMocks.createFnAgent.mockClear();
|
||||
piMocks.resolvePlanningThinkingLevel.mockClear();
|
||||
|
||||
const retriedRes = await request(app, "POST", `/api/insights/runs/${failedRun.id}/retry`, JSON.stringify({}), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
expect(retriedRes.status).toBe(201);
|
||||
|
||||
expect(piMocks.resolvePlanningThinkingLevel).toHaveBeenCalledWith(expect.any(Object), "xhigh");
|
||||
expect(piMocks.createFnAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
defaultThinkingLevel: "xhigh",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("POST /api/insights/run passes explicit model override to createFnAgent", async () => {
|
||||
const res = await request(
|
||||
app,
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { TaskStore } from "@fusion/core";
|
||||
import {
|
||||
InsightLifecycleError,
|
||||
InsightStore,
|
||||
THINKING_LEVELS,
|
||||
executeInsightRunLifecycle,
|
||||
resolvePlanningSettingsModel,
|
||||
retryInsightRunLifecycle,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
type InsightRunListOptions,
|
||||
type InsightRunStatus,
|
||||
type Settings,
|
||||
type ThinkingLevel,
|
||||
} from "@fusion/core";
|
||||
import {
|
||||
ApiError,
|
||||
@@ -41,7 +43,7 @@ import {
|
||||
startInsightRunSweeper,
|
||||
sweepStaleInsightRuns,
|
||||
} from "./insight-run-sweeper.js";
|
||||
import { createFnAgent, promptWithFallback, resolveMcpServersForStore } from "@fusion/engine";
|
||||
import { createFnAgent, promptWithFallback, resolveMcpServersForStore, resolvePlanningThinkingLevel } from "@fusion/engine";
|
||||
|
||||
/**
|
||||
* Re-throws an error as an ApiError, converting unknown errors to internal errors.
|
||||
@@ -133,6 +135,7 @@ async function executeInsightAttempt(params: {
|
||||
settings: Settings;
|
||||
modelProvider?: string;
|
||||
modelId?: string;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
}): Promise<{ summary: string; insightsCreated: number; insightsUpdated: number }> {
|
||||
const {
|
||||
readWorkingMemory,
|
||||
@@ -157,6 +160,7 @@ async function executeInsightAttempt(params: {
|
||||
const hasCustomModel = params.modelProvider && params.modelId;
|
||||
const fallbackProvider = hasCustomModel ? settingsProvider : undefined;
|
||||
const fallbackModelId = hasCustomModel ? settingsModelId : undefined;
|
||||
const effectiveThinkingLevel = resolvePlanningThinkingLevel(params.settings, params.thinkingLevel);
|
||||
|
||||
const existingInsights = await readInsightsMemory(params.rootDir);
|
||||
const mcpServers = (await resolveMcpServersForStore(params.taskStore ?? {})).servers;
|
||||
@@ -172,6 +176,11 @@ async function executeInsightAttempt(params: {
|
||||
defaultModelId: finalModelId,
|
||||
fallbackProvider,
|
||||
fallbackModelId,
|
||||
/*
|
||||
* FNXC:Insights-ThinkingLevel 2026-07-12-19:24:
|
||||
* Insight generation now persists a per-run reasoning-effort selection in inputMetadata.metadata.thinkingLevel and resolves it through the planning lane so manual runs and retries honor the same operator choice.
|
||||
*/
|
||||
defaultThinkingLevel: effectiveThinkingLevel,
|
||||
systemPrompt: [
|
||||
"You extract durable project insights from working memory notes.",
|
||||
"Return only valid JSON that matches the requested schema.",
|
||||
@@ -385,20 +394,33 @@ export function createInsightsRouter(store: TaskStore): Router {
|
||||
const settings = await taskStore.getSettings();
|
||||
const rawProvider = typeof req.body.modelProvider === "string" ? req.body.modelProvider.trim() : undefined;
|
||||
const rawModelId = typeof req.body.modelId === "string" ? req.body.modelId.trim() : undefined;
|
||||
const rawThinkingLevel = typeof req.body.thinkingLevel === "string" ? req.body.thinkingLevel.trim() : undefined;
|
||||
const thinkingLevel = rawThinkingLevel
|
||||
? THINKING_LEVELS.includes(rawThinkingLevel as ThinkingLevel)
|
||||
? rawThinkingLevel as ThinkingLevel
|
||||
: undefined
|
||||
: undefined;
|
||||
if (rawThinkingLevel && !thinkingLevel) {
|
||||
throw badRequest(`Invalid thinkingLevel: ${rawThinkingLevel}`);
|
||||
}
|
||||
// Require both provider and model ID together — partial values are discarded
|
||||
const modelProvider = rawProvider && rawModelId ? rawProvider : undefined;
|
||||
const modelId = rawProvider && rawModelId ? rawModelId : undefined;
|
||||
const controller = new AbortController();
|
||||
|
||||
// Stash model selection in inputMetadata.metadata so retries can recover it
|
||||
/*
|
||||
* FNXC:Insights-ThinkingLevel 2026-07-12-19:24:
|
||||
* Insight runs store the operator's model and reasoning-effort selection in inputMetadata.metadata instead of adding schema columns, because retries must recover exactly the per-run values used for generation.
|
||||
*/
|
||||
const inputMetadata = typeof req.body.inputMetadata === "object" && req.body.inputMetadata !== null
|
||||
? { ...req.body.inputMetadata }
|
||||
: {};
|
||||
if (modelProvider || modelId) {
|
||||
if (modelProvider || modelId || thinkingLevel) {
|
||||
inputMetadata.metadata = {
|
||||
...(typeof inputMetadata.metadata === "object" && inputMetadata.metadata !== null ? inputMetadata.metadata : {}),
|
||||
...(modelProvider ? { modelProvider } : {}),
|
||||
...(modelId ? { modelId } : {}),
|
||||
...(thinkingLevel ? { thinkingLevel } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -434,6 +456,7 @@ export function createInsightsRouter(store: TaskStore): Router {
|
||||
settings,
|
||||
modelProvider,
|
||||
modelId,
|
||||
thinkingLevel,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -612,7 +635,7 @@ export function createInsightsRouter(store: TaskStore): Router {
|
||||
const settings = await taskStore.getSettings();
|
||||
const controller = new AbortController();
|
||||
|
||||
// Recover model selection from the original run's inputMetadata
|
||||
// Recover model and reasoning-effort selection from the original run's inputMetadata.
|
||||
const originalMetadata = existing.inputMetadata?.metadata;
|
||||
const retryModelProvider = typeof (originalMetadata as Record<string, unknown> | undefined)?.modelProvider === "string"
|
||||
? (originalMetadata as Record<string, unknown>).modelProvider as string
|
||||
@@ -620,6 +643,10 @@ export function createInsightsRouter(store: TaskStore): Router {
|
||||
const retryModelId = typeof (originalMetadata as Record<string, unknown> | undefined)?.modelId === "string"
|
||||
? (originalMetadata as Record<string, unknown>).modelId as string
|
||||
: undefined;
|
||||
const retryThinkingLevel = typeof (originalMetadata as Record<string, unknown> | undefined)?.thinkingLevel === "string"
|
||||
&& THINKING_LEVELS.includes((originalMetadata as Record<string, unknown>).thinkingLevel as ThinkingLevel)
|
||||
? (originalMetadata as Record<string, unknown>).thinkingLevel as ThinkingLevel
|
||||
: undefined;
|
||||
|
||||
const { run } = await retryInsightRunLifecycle({
|
||||
store,
|
||||
@@ -640,6 +667,7 @@ export function createInsightsRouter(store: TaskStore): Router {
|
||||
settings,
|
||||
modelProvider: retryModelProvider,
|
||||
modelId: retryModelId,
|
||||
thinkingLevel: retryThinkingLevel,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -858,6 +858,7 @@ export {
|
||||
describeAgentModel,
|
||||
resolveExecutorThinkingLevel,
|
||||
resolveExecutorFallbackThinkingLevel,
|
||||
resolvePlanningThinkingLevel,
|
||||
resolvePlanningFallbackThinkingLevel,
|
||||
resolveValidatorFallbackThinkingLevel,
|
||||
resolveTitleSummarizerFallbackThinkingLevel,
|
||||
|
||||
Reference in New Issue
Block a user