Skip to content

Fix Default Model Bug #161

@ofilson

Description

@ofilson

Bug: Default model selection does not persist across page reloads

Summary

When a user selects a default model in Settings > Chat Preferences, the selection appears to save (no API errors), but after reloading the page the dropdown reverts to "No default (use first available)". The setting is not visually restored despite being stored in DynamoDB.

Environment

  • Frontend: Angular v21, standalone components, Angular Signals
  • Backend: FastAPI, DynamoDB (user settings table)
  • Affected page: /settings/chat — "Default model" dropdown

Steps to Reproduce

  1. Log in and navigate to Settings > Chat Preferences.
  2. Open the "Default model" dropdown.
  3. Select any model (e.g. "Claude Haiku 4.5 (Anthropic)").
  4. Observe: no error is shown, "Saving..." indicator appears and disappears.
  5. Reload the page (F5 or browser refresh).
  6. Observe: the dropdown shows "No default (use first available)" instead of the previously selected model.

Expected: The dropdown should show the model selected in step 3.

Actual: The dropdown resets to the empty/default option every time.

Root Cause Analysis

There are two distinct issues contributing to this bug — one on the frontend (display) and one on the backend (usage at chat time).

Issue 1: Frontend — <select> binding with Angular Signals and resource()

The chat preferences component uses Angular's resource() API to load settings:

// user-settings.service.ts
readonly settingsResource = resource({
  loader: async () => this.fetchSettings(),
});

The component binds the dropdown value via a computed signal:

// chat-preferences-settings.page.ts
readonly currentDefaultModelId = computed(() => {
  const settings = this.userSettingsService.settingsResource.value();
  return settings?.defaultModelId ?? '';
});

And the template uses [value] binding on the <select>:

<select [value]="currentDefaultModelId()" (change)="onModelChange($event)">
  <option value="">No default (use first available)</option>
  @for (model of modelService.availableModels(); track model.id) {
    <option [value]="model.modelId">{{ model.modelName }} ({{ model.providerName }})</option>
  }
</select>

The problem is a race condition between two async data sources:

  1. settingsResource loads the user's defaultModelId from the API.
  2. modelService.availableModels() loads the model list from a separate API call.

If the settings resolve before the model list, the <select> tries to set its value to a modelId that doesn't have a matching <option> yet (because the @for loop hasn't rendered). The browser silently resets the <select> to the first option (""). When the models finally load and the options render, Angular doesn't re-apply the [value] binding because the computed signal value hasn't changed.

Additionally, [value] on a native <select> is a one-time DOM property set — it doesn't reactively re-sync when options are added later.

Issue 2: Backend — User's default model is never applied at chat time

When the user sends a message without explicitly selecting a model in the session, the frontend sends model_id: null to the backend. The chat stream endpoint in backend/src/apis/app_api/chat/routes.py creates the agent without looking up the user's saved default:

# chat/routes.py — current behavior
agent = get_agent(
    session_id=request.session_id,
    user_id=user_id,
    enabled_tools=authorized_tools,
    system_prompt=system_prompt,
    # model_id is NOT passed — user's default is never loaded
)

The agent falls back to the hardcoded system default (us.anthropic.claude-haiku-4-5-20251001-v1:0) instead of the user's saved preference.

Proposed Solutions

Fix 1: Frontend — Ensure dropdown reflects persisted value after both data sources load

Option A — Use a computed that depends on both signals:

Create a combined readiness signal that only emits the defaultModelId once both the settings and the model list have loaded:

readonly currentDefaultModelId = computed(() => {
  const settings = this.userSettingsService.settingsResource.value();
  const models = this.modelService.availableModels();
  if (!settings || models.length === 0) return '';
  return settings.defaultModelId ?? '';
});

This ensures the value is only set after the <option> elements exist in the DOM.

Option B — Switch to reactive form control or ngModel:

Replace the native [value] binding with Angular's ngModel or a reactive FormControl, which handles async option population correctly:

<select [ngModel]="currentDefaultModelId()" (ngModelChange)="onModelChange($event)">

Option C — Force re-render after models load:

After the model list loads, trigger a change detection cycle or manually set the select element's value via ViewChild.

Recommended: Option A is the simplest and stays within the existing signal-based architecture.

Fix 2: Backend — Load user's default model when model_id is null

In the chat stream endpoint, when request.model_id is None, look up the user's saved defaultModelId from the settings table and pass it to the agent:

# chat/routes.py — proposed fix
model_id = request.model_id
provider = request.provider

if model_id is None:
    user_settings = await user_settings_repo.get_settings(user_id)
    saved_default = user_settings.get("defaultModelId")
    if saved_default:
        model_id = saved_default
        # Optionally resolve provider from managed models table

agent = get_agent(
    session_id=request.session_id,
    user_id=user_id,
    enabled_tools=authorized_tools,
    system_prompt=system_prompt,
    model_id=model_id,
    provider=provider,
)

Fix 3: Frontend — Apply user's default model to session model selector

The ModelService.loadModels() method currently resolves the selected model with this priority:

  1. In-memory selection
  2. sessionStorage (tab-scoped)
  3. Admin-configured default (isDefault: true on the model)
  4. First available model

The user's personal default from the settings API is never consulted. Add it as priority 3 (after sessionStorage, before admin default):

// model.service.ts — proposed addition in loadModels()
} else if (enabledModels.length > 0) {
  const savedModelId = this.getSavedModelId();
  const savedModel = savedModelId ? enabledModels.find(m => m.modelId === savedModelId) : null;

  if (savedModel) {
    this._selectedModel.set(savedModel);
    this.usingDefaultModel.set(false);
  } else {
    // NEW: Check user's persisted default from settings API
    const userSettings = await firstValueFrom(
      this.http.get<UserSettings>(userSettingsUrl)
    );
    const userDefault = userSettings?.defaultModelId
      ? enabledModels.find(m => m.modelId === userSettings.defaultModelId)
      : null;

    if (userDefault) {
      this._selectedModel.set(userDefault);
      this.usingDefaultModel.set(false);
    } else {
      // Fall back to admin default or first model
      const defaultModel = enabledModels.find(m => m.isDefault);
      this._selectedModel.set(defaultModel || enabledModels[0]);
      this.usingDefaultModel.set(false);
    }
  }
}

Affected Files

File Role
frontend/.../chat-preferences-settings.page.ts Settings UI — dropdown binding (Fix 1)
frontend/.../services/user-settings.service.ts Settings API client
frontend/.../services/model/model.service.ts Model selection logic (Fix 3)
backend/.../app_api/chat/routes.py Chat endpoint — agent creation (Fix 2)
backend/.../shared/user_settings/repository.py DynamoDB settings read/write

Priority

Medium — The feature silently fails (no errors in console or network), so users believe their preference was saved when it wasn't. This affects both the settings display and the actual model used during chat.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggood first issueGood for newcomers

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions