Fix chat model fallback when preferred model is unavailable Updates the model selection and chat request logic to verify that the user's preferred model exists in the list of available models. If the preferred model is missing or removed from the backend, the logic now gracefully falls back to the default model. This prevents the chat interface from entering an invalid or stuck loading state. Google-Bug-Id: b/493583484 Release-Notes: skip Change-Id: I75094cfc45eff31260884ea4fceba3a7f6e8b383
diff --git a/polygerrit-ui/app/models/chat/chat-model.ts b/polygerrit-ui/app/models/chat/chat-model.ts index e32aaa0..ffd12fe 100644 --- a/polygerrit-ui/app/models/chat/chat-model.ts +++ b/polygerrit-ui/app/models/chat/chat-model.ts
@@ -24,6 +24,7 @@ Reference, } from '../../api/ai-code-review'; import {ChangeInfo, CommentInfo, FileInfoStatus} from '../../api/rest-api'; +import {PreferencesInfo} from '../../types/common'; import {isDefined} from '../../types/types'; import {assert, assertIsDefined, cryptoUuid} from '../../utils/common-util'; import {select} from '../../utils/observable-util'; @@ -355,9 +356,7 @@ this.userModel.preferences$.pipe(startWith(undefined)), ]), ([chatState, preferences]) => - chatState.selectedModelId ?? - preferences?.ai_chat_selected_model ?? - chatState.models?.default_model_id + this.getEffectiveModelId(chatState, preferences) ); this.selectedModel$ = select( @@ -414,6 +413,21 @@ }); } + private getEffectiveModelId( + state: ChatState, + preferences?: PreferencesInfo + ): string | undefined { + const id = + state.selectedModelId ?? + preferences?.ai_chat_selected_model ?? + state.models?.default_model_id; + + if (!state.models?.models) return id; + + const isAvailable = state.models.models.some(m => m.model_id === id); + return isAvailable ? id : state.models.default_model_id; + } + contextItemToType(contextItem?: ContextItem): ContextItemType | undefined { if (!contextItem) return undefined; const state = this.getState(); @@ -560,7 +574,10 @@ turn_index: turnIndex, regeneration_index: turn.geminiMessage.regenerationIndex, client_data: JSON.stringify(clientData), - model_name: state.selectedModelId ?? state.models.default_model_id, + model_name: this.getEffectiveModelId( + state, + this.userModel.getState().preferences + ), external_contexts: contextItems, }; const listener: ChatResponseListener = {
diff --git a/polygerrit-ui/app/models/chat/chat-model_test.ts b/polygerrit-ui/app/models/chat/chat-model_test.ts index 85322f3..dc6b03a 100644 --- a/polygerrit-ui/app/models/chat/chat-model_test.ts +++ b/polygerrit-ui/app/models/chat/chat-model_test.ts
@@ -42,6 +42,9 @@ } as unknown as FilesModel; updatePreferencesStub = sinon.stub(); userModel = { + getState: () => { + return {preferences: {}}; + }, preferences$: new BehaviorSubject({}), updatePreferences: updatePreferencesStub, } as unknown as UserModel; @@ -191,6 +194,66 @@ assert.equal(request.model_name, 'advanced-model'); }); + test('selectedModelId$ falls back when preferred model is unavailable', async () => { + const models = { + models: [ + { + model_id: 'default-model', + }, + ], + default_model_id: 'default-model', + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (provider.getModels as sinon.SinonStub).resolves(models as any); + + changeModel.updateStateChange(createParsedChange()); + await new Promise(resolve => setTimeout(resolve, 0)); + + model.selectModel('removed-model'); + + let selectedModelId; + const sub = model.selectedModelId$.subscribe(id => (selectedModelId = id)); + assert.equal(selectedModelId, 'default-model'); + sub.unsubscribe(); + }); + + test('chat falls back to default model when selected model is unavailable', async () => { + const models = { + models: [ + { + model_id: 'default-model', + }, + ], + default_model_id: 'default-model', + }; + const actions = { + actions: [ + { + id: 'default-action', + display_text: 'Default Action', + initial_user_prompt: 'Hello', + }, + ], + default_action_id: 'default-action', + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (provider.getActions as sinon.SinonStub).resolves(actions as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (provider.getModels as sinon.SinonStub).resolves(models as any); + + changeModel.updateStateChange(createParsedChange()); + await new Promise(resolve => setTimeout(resolve, 0)); + + model.selectModel('removed-model'); + + model.updateUserInput('hello'); + model.chat('hello', undefined, 0); + + const request = (provider.chat as sinon.SinonStub).lastCall + .args[0] as ChatRequest; + assert.equal(request.model_name, 'default-model'); + }); + test('change navigation resets state', () => { model.updateUserInput('some input'); model.selectModel('some-model');