Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions backend/src/apis/app_api/chat/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,36 @@ async def chat_stream(request: ChatRequest, current_user: User = Depends(get_cur
logger.warning(f"Failed to resolve file upload IDs: {e}")
# Continue without files rather than failing the request

# Resolve model selection: if the request omitted a model_id, fall back to the
# user's persisted default from settings before letting get_agent use the
# hardcoded system default. Provider is resolved from the managed models table
# when possible so the correct client/formatter is used.
model_id = request.model_id
provider = request.provider

if model_id is None:
try:
from apis.shared.user_settings.repository import UserSettingsRepository

user_settings_repo = UserSettingsRepository()
user_settings = await user_settings_repo.get_settings(user_id)
saved_default = user_settings.get("defaultModelId")
if saved_default:
model_id = saved_default
if provider is None:
try:
from apis.shared.models.managed_models import get_managed_model

managed = await get_managed_model(saved_default)
if managed is not None:
provider = getattr(managed, "provider", None) or provider
except Exception as provider_err:
logger.debug(f"Could not resolve provider for {saved_default}: {provider_err}")
logger.info(f"Applied user's persisted default model {saved_default} for user {user_id}")
except Exception as e:
logger.error(f"Error loading user default model for user {user_id}: {e}", exc_info=True)
# Continue without saved default - agent will use system default

try:
# Get agent instance (with or without tool filtering)
# Use assistant's system prompt if provided
Expand All @@ -368,6 +398,8 @@ async def chat_stream(request: ChatRequest, current_user: User = Depends(get_cur
user_id=user_id,
enabled_tools=authorized_tools, # Filtered by RBAC (may be None for all allowed)
system_prompt=system_prompt, # Assistant instructions if assistant is attached
model_id=model_id,
provider=provider,
)

# Wrap stream to ensure flush on disconnect and prevent further processing
Expand Down
42 changes: 39 additions & 3 deletions frontend/ai.client/src/app/session/services/model/model.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { ConfigService } from '../../../services/config.service';
import { AuthService } from '../../../auth/auth.service';
import { UserSettingsService } from '../../../services/user-settings.service';
import { ManagedModel } from '../../../admin/manage-models/models/managed-model.model';

interface ManagedModelsListResponse {
Expand All @@ -17,6 +18,7 @@ export class ModelService {
private http = inject(HttpClient);
private authService = inject(AuthService);
private config = inject(ConfigService);
private userSettingsService = inject(UserSettingsService);
private readonly baseUrl = computed(() => `${this.config.appApiUrl()}/models`);

// Session storage key for persisting model selection
Expand Down Expand Up @@ -128,10 +130,16 @@ export class ModelService {
this._selectedModel.set(savedModel);
this.usingDefaultModel.set(false);
} else {
// Find admin-configured default model, or fall back to first available
const defaultModel = enabledModels.find(m => m.isDefault);
this._selectedModel.set(defaultModel || enabledModels[0]);
// Set provisional selection synchronously so the UI has a value immediately.
// Priority: admin-configured default (isDefault: true), else first available.
const adminDefault = enabledModels.find(m => m.isDefault);
this._selectedModel.set(adminDefault || enabledModels[0]);
this.usingDefaultModel.set(false);

// Then asynchronously upgrade to the user's persisted default if one is set.
// We don't await inside the synchronous selection path so callers (and tests)
// see a valid selection without waiting on the settings HTTP call.
this.applyUserDefaultModel(enabledModels);
}
} else {
// No models available, use system default
Expand Down Expand Up @@ -221,6 +229,34 @@ export class ModelService {
}
}

/**
* Asynchronously applies the user's persisted default model (from the settings API)
* on top of an already-set provisional selection. Runs as a fire-and-forget task.
* Only upgrades the selection if:
* - The user has a defaultModelId set
* - That model exists in the supplied enabled list
* - The user hasn't already made a different explicit selection while loading
*/
private async applyUserDefaultModel(enabledModels: ManagedModel[]): Promise<void> {
const selectionBeforeFetch = this._selectedModel();
try {
const settings = await this.userSettingsService.fetchSettings();
const userDefaultId = settings?.defaultModelId;
if (!userDefaultId) return;

// Skip if the user changed the selection while we were fetching.
if (this._selectedModel() !== selectionBeforeFetch) return;

const userDefault = enabledModels.find(m => m.modelId === userDefaultId);
if (userDefault) {
this._selectedModel.set(userDefault);
this.usingDefaultModel.set(false);
}
} catch (e) {
console.warn('Could not load user default model from settings:', e);
}
}

/**
* Retrieves the saved model ID from sessionStorage
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@ export class ChatPreferencesSettingsPage {

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

async onModelChange(event: Event): Promise<void> {
Expand Down