Skip to content

fix(dashboard): resolve default model from settings for mission interview#49

Merged
gsxdsm merged 1 commit intoRunfusion:mainfrom
timothyjlaurent:fix/mission-interview-default-model
May 6, 2026
Merged

fix(dashboard): resolve default model from settings for mission interview#49
gsxdsm merged 1 commit intoRunfusion:mainfrom
timothyjlaurent:fix/mission-interview-default-model

Conversation

@timothyjlaurent
Copy link
Copy Markdown
Contributor

@timothyjlaurent timothyjlaurent commented May 6, 2026

Summary

  • Mission interview route now resolves the default model from settings when "Use default" is selected, instead of passing undefined for both modelProvider and modelId
  • Uses resolvePlanningSettingsModel() which chains through: planning-specific → project defaults → global defaults
  • Explicit model overrides from the request body still take precedence

Root Cause

POST /api/missions/interview/start in mission-routes.ts passed the request body's modelProvider/modelId directly to createMissionInterviewSession without resolving defaults. When "Use default" was selected, both were undefined, causing createFnAgent to use pi's internal fallback instead of the user's configured default model (e.g. zai/glm-5.1). This produced "AI returned no valid JSON. Please try again."

Test plan

  • 4 new unit tests: global default resolution, planning-specific model priority, explicit override precedence, no-defaults fallback
  • All 9 existing interview scoping tests still pass

Closes #48

🤶 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Tests

    • Added comprehensive test coverage for mission interview initialization with project-specific model settings, including resolution precedence and default handling.
  • Bug Fixes

    • Mission interviews now correctly resolve and apply project-specific AI model configuration when starting sessions.

…view

The mission interview route passed modelProvider/modelId from the request
body directly without resolving the configured default model from settings.
When "Use default" was selected, both values were undefined, causing
createFnAgent to use pi's internal fallback instead of the user's
configured default (e.g. zai/glm-5.1). This produced "AI returned no
valid JSON" errors.

Use resolvePlanningSettingsModel() to resolve the effective model from
the settings hierarchy (planning-specific → project → global defaults),
with explicit request overrides still taking precedence.

Closes Runfusion#48

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

This PR fixes mission interview to respect configured default models by using resolvePlanningSettingsModel() to resolve effective provider and model ID when no explicit override is provided. Four new tests verify model resolution from settings, planning-specific overrides, explicit override precedence, and undefined model fallback.

Changes

Mission Interview Default Model Resolution

Layer / File(s) Summary
Import Addition
packages/dashboard/src/mission-routes.ts
resolvePlanningSettingsModel imported from @fusion/core to support model resolution from settings.
Core Logic
packages/dashboard/src/mission-routes.ts
In /interview/start handler, compute effective model via resolvePlanningSettingsModel(settings), then resolve provider and modelId by chaining explicit inputs with effective model defaults before passing to createMissionInterviewSession.
Test Coverage
packages/dashboard/src/__tests__/mission-e2e.test.ts
Four new tests verify: (1) global default model resolution when no override, (2) planning-specific model override precedence, (3) explicit override taking precedence over settings, (4) undefined model when no defaults configured.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • gsxdsm

Poem

🐰 A bunny hops through settings deep,
Where models rest and defaults sleep,
With resolvePlanningSettingsModel's call,
The interview uses the best one of all!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main change: resolving the default model from settings for mission interview, which is the core purpose of the PR.
Linked Issues check ✅ Passed The PR fully implements all objectives from issue #48: uses resolvePlanningSettingsModel() for default resolution, preserves explicit overrides, and adds comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the mission interview default model resolution issue; no extraneous modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/dashboard/src/mission-routes.ts (1)

402-419: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize model overrides before ?? fallback to prevent empty-string override bugs.

"" values currently bypass the “both-or-neither” intent and still override resolved defaults via ??, which can pass invalid model settings into createMissionInterviewSession.

Suggested fix
-        const effectiveModel = resolvePlanningSettingsModel(settings);
-        const resolvedProvider = modelProvider ?? effectiveModel.provider;
-        const resolvedModelId = modelId ?? effectiveModel.modelId;
+        const normalizedProvider = typeof modelProvider === "string" && modelProvider.trim().length > 0
+          ? modelProvider.trim()
+          : undefined;
+        const normalizedModelId = typeof modelId === "string" && modelId.trim().length > 0
+          ? modelId.trim()
+          : undefined;
+
+        if ((normalizedProvider === undefined) !== (normalizedModelId === undefined)) {
+          throw badRequest("Both modelProvider and modelId must be provided together, or neither should be provided");
+        }
+
+        const effectiveModel = resolvePlanningSettingsModel(settings);
+        const resolvedProvider = normalizedProvider ?? effectiveModel.provider;
+        const resolvedModelId = normalizedModelId ?? effectiveModel.modelId;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashboard/src/mission-routes.ts` around lines 402 - 419, Normalize
incoming model override strings (treat empty string as undefined) before
applying the both-or-neither check and the nullish-coalescing fallback: create
normalizedModelProvider and normalizedModelId (convert "" to undefined, e.g.
trim and if empty set undefined), use those normalized variables in the existing
conditional that throws the badRequest and in computing resolvedProvider and
resolvedModelId (where resolvePlanningSettingsModel(settings) is used). This
ensures empty-string overrides do not bypass validation or win over defaults
when computing resolvedProvider/resolvedModelId used for
createMissionInterviewSession.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/dashboard/src/mission-routes.ts`:
- Around line 402-419: Normalize incoming model override strings (treat empty
string as undefined) before applying the both-or-neither check and the
nullish-coalescing fallback: create normalizedModelProvider and
normalizedModelId (convert "" to undefined, e.g. trim and if empty set
undefined), use those normalized variables in the existing conditional that
throws the badRequest and in computing resolvedProvider and resolvedModelId
(where resolvePlanningSettingsModel(settings) is used). This ensures
empty-string overrides do not bypass validation or win over defaults when
computing resolvedProvider/resolvedModelId used for
createMissionInterviewSession.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5d9c86af-4ccd-4692-a070-aa869db576be

📥 Commits

Reviewing files that changed from the base of the PR and between b4b7a8a and 5fd678d.

📒 Files selected for processing (2)
  • packages/dashboard/src/__tests__/mission-e2e.test.ts
  • packages/dashboard/src/mission-routes.ts

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 6, 2026

Greptile Summary

This fix ensures the POST /api/missions/interview/start route resolves the user's configured default model from settings (via resolvePlanningSettingsModel) instead of silently passing undefined when "Use default" is selected, eliminating the "AI returned no valid JSON" error.

  • mission-routes.ts: Calls resolvePlanningSettingsModel(settings) after fetching settings and uses ?? to let explicit request-body values take precedence; the pre-existing paired-fields validation guarantees the two ?? expressions never produce a mismatched provider/model pair.
  • mission-e2e.test.ts: Adds 4 tests covering global-default resolution, planning-specific priority, explicit-override precedence, and the no-defaults passthrough; the planningGlobalProvider/planningGlobalModelId mid-tier of the chain is not yet exercised at the route layer.

Confidence Score: 4/5

Safe to merge — the change is a narrow, well-tested fix that wires an already-existing resolver into the interview route.

The production code path is correct: resolvePlanningSettingsModel already has its own tests, the ?? operators interact cleanly with the pre-existing paired-fields validation, and all four major resolution scenarios are covered by new tests. The only gap is that the planningGlobalProvider/planningGlobalModelId tier of the resolution chain is not exercised through the route layer, which is a test-coverage gap rather than a defect in the shipped code.

The new tests in mission-e2e.test.ts would benefit from an additional case covering the planningGlobalProvider/planningGlobalModelId settings tier; mission-routes.ts itself is clean.

Important Files Changed

Filename Overview
packages/dashboard/src/mission-routes.ts Adds resolvePlanningSettingsModel call before passing model parameters to createMissionInterviewSession, ensuring the user-configured default model is used when no explicit override is sent. Logic is correct: ?? only falls back when the request value is null/undefined, and the pre-existing paired-fields validation ensures both fields arrive together or not at all.
packages/dashboard/src/tests/mission-e2e.test.ts Adds 4 new tests covering: global-default resolution, planning-specific model priority, explicit-override precedence, and the no-defaults passthrough. The planningGlobalProvider/planningGlobalModelId mid-tier of the resolution chain is not exercised through the route layer.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Route as POST /interview/start
    participant Store as TaskStore
    participant Resolver as resolvePlanningSettingsModel
    participant Session as createMissionInterviewSession

    Client->>Route: { missionTitle, modelProvider?, modelId? }
    Route->>Store: getSettings()
    Store-->>Route: settings
    Route->>Resolver: resolvePlanningSettingsModel(settings)
    Note over Resolver: planningProvider/modelId<br/>→ planningGlobalProvider/modelId<br/>→ defaultProvider/modelId
    Resolver-->>Route: { provider?, modelId? }
    Note over Route: resolvedProvider = modelProvider ?? effectiveModel.provider<br/>resolvedModelId = modelId ?? effectiveModel.modelId
    Route->>Session: (ip, title, rootDir, overrides, resolvedProvider, resolvedModelId)
    Session-->>Route: sessionId
    Route-->>Client: 201 { sessionId }
Loading

Reviews (1): Last reviewed commit: "fix(dashboard): resolve default model fr..." | Re-trigger Greptile

Comment on lines +2919 to +2957

it("POST /api/missions/interview/start uses planning-specific model over global default", async () => {
// Configure scoped store with both planning-specific and global defaults
scopedStore = {
getRootDir: vi.fn().mockReturnValue(scopedRootDir),
getSettings: vi.fn().mockResolvedValue({
promptOverrides: {},
planningProvider: "anthropic",
planningModelId: "claude-sonnet-4-5",
defaultProvider: "zai",
defaultModelId: "glm-5.1",
}),
getMissionStore: vi.fn().mockReturnValue(createMockMissionStore()),
} as unknown as TaskStore;
vi.spyOn(projectStoreResolver, "getOrCreateProjectStore").mockResolvedValue(scopedStore);

const createSpy = vi
.spyOn(missionInterviewModule, "createMissionInterviewSession")
.mockResolvedValueOnce("planning-model-session-id");

const { app } = buildApp();
const res = await request(
app,
"POST",
`/api/missions/interview/start?projectId=${projectId}`,
JSON.stringify({ missionTitle: "Planning Model Mission" }),
{ "content-type": "application/json" }
);

expect(res.status).toBe(201);
// Planning-specific model should take priority over global default
expect(createSpy).toHaveBeenCalledWith(
expect.any(String),
"Planning Model Mission",
scopedRootDir,
{},
"anthropic",
"claude-sonnet-4-5",
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Global-planning tier not covered by route tests

The new tests exercise tier 1 (project-level planningProvider/planningModelId) and tier 4 (defaultProvider/defaultModelId), but the intermediate planningGlobalProvider/planningGlobalModelId tier in resolvePlanningSettingsModel has no route-layer coverage. If the resolver's priority order is ever changed, a bug could silently survive. Adding a test with only planningGlobalProvider/planningGlobalModelId set (and no project-level planning fields) would close this gap.

@gsxdsm gsxdsm merged commit 7f441ea into Runfusion:main May 6, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(dashboard): mission interview ignores configured default model

2 participants