fix: prevent BYOK model from conflicting with same-named Copilot model in picker#246
fix: prevent BYOK model from conflicting with same-named Copilot model in picker#246rsd-darshan wants to merge 12 commits into
Conversation
…l in picker When a BYOK model is named identically to a Copilot-native model (e.g. GPT-4.1), both dropdown items received the same ID, causing the picker to highlight both as selected simultaneously. Use a provider-prefixed picker ID for BYOK models so each item has a unique identity, and update the selection and lookup logic to match. Fixes microsoft#167
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Fixes model picker selection conflicts when a BYOK model shares the same display name as a Copilot-native model by introducing a unique picker identity and using it consistently across picker item creation, selection display, and model lookup.
Changes:
- Added
ModelUtils.getPickerId()to generate unique IDs for picker items (BYOK:providerName_modelName, native:modelName). - Updated model lookup in
ModelService#setActiveModelto match by picker ID. - Updated picker binding and dropdown item construction to use the new picker ID.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/ModelUtils.java | Adds getPickerId() helper to generate unique picker IDs. |
| com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ModelService.java | Uses picker ID for active model lookup and selected item display. |
| com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ModelPickerGroupsBuilder.java | Uses picker ID as dropdown item id to prevent collisions. |
Comments suppressed due to low confidence (1)
com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ModelService.java:1
setActiveModel(String modelName)now effectively expects a picker ID (sometimesproviderName_modelName) rather than a plain model name. This is a behavioral/API contract change that can break existing call sites that passgetModelName(). Consider (a) renaming the parameter/method to reflect the new meaning (e.g.,setActiveModelByPickerId(String pickerId)), or (b) supporting both formats for backward compatibility (first match by picker ID, then fallback match bygetModelName()when no picker-id match is found).
// Copyright (c) Microsoft Corporation.
Existing internal callers pass plain model names (e.g. fallback model, custom mode event). Match by picker ID first, then fall back to model name so both call patterns keep working.
|
Hi @rsd-darshan, thank you for your contribution. Pls check the copilot review first :) |
…concatenation Replaces the custom providerName + "_" + modelName concatenation with the model's existing getModelKey() method, which already produces a stable composite key used throughout the codebase. This avoids potential collisions from naive string concatenation and eliminates the blank-provider-name edge case.
|
@jdneo addressed the Copilot review comments — switched to using the model's existing |
…dules (microsoft#208) Replace getPickerId() wrapper with direct model.getModelKey() calls at each use site, removing the unnecessary indirection.
- setActiveModel now accepts only a composite model key (modelKey), matching the picker ID used throughout the codebase - Custom mode event handler resolves the model name to a key via findModelKeyByName() before calling setActiveModel - ModelHoverContentProvider uses model.getModelKey() directly instead of model.getModelName()
|
@ethanyhou Currently we have fallback model related logics in ModelService. Is it still require after TBB rolls out? |
ethanyhou
left a comment
There was a problem hiding this comment.
Review (Google CL guidelines)
Overall the fix is on the right track and clearly improves code health: getModelKey() already exists on CopilotModel and is the natural unique identifier for the picker — replacing getModelName() with it as the dropdown id is the correct, minimal fix for #167. Nice job reusing the existing helper rather than inventing a new ID format (the later refactor commit a8f7a38b is a good cleanup).
Blocking
-
customModeModelChangedEventHandlerretains the same-name ambiguity the rest of the PR fixes (ModelService.java~L137–155 +findModelKeyByName~L334). The event payload is a model name, andfindModelKeyByNamereturns the first map entry whosegetModelName()matches — so if a BYOK model shares a name with a Copilot-native model (the exact scenario in #167), this path can still activate the wrong one. Two options, either is fine:- Document the limitation with a
// TODOreferencing a follow-up bug to extend the custom-mode protocol to carry the composite key (or provider + name), or - Disambiguate using
modelFamilyalready parsed out of"<modelName> (<modelFamily>)"(currently the family is computed and discarded — seeopenParenIndexlogic).
Per the "no deferred cleanup without a filed bug" guideline, please at minimum file an issue and reference it in a code comment.
- Document the limitation with a
-
No tests. This is a regression-prone area (selection identity flowing through dropdown id ↔ map key ↔ persisted preference), and the bug it fixes is exactly the kind a unit test catches cheaply.
ModelPickerGroupsBuilder.buildModelDropdownItemsreturns plainDropdownItems and should be testable incom.microsoft.copilot.eclipse.ui.testwithout SWT. Please add at least one test asserting that twoCopilotModels sharing amodelNameproduce twoDropdownItems with distinctids. Bonus: aModelService.setActiveModeltest covering same-named BYOK + native models.
Non-blocking
-
Nit: PR description is stale. It still describes a
ModelUtils.getPickerId()helper returningproviderName_modelName, but the implemented code uses the existingCopilotModel.getModelKey()(which isproviderName + "_" + id, notproviderName + "_" + modelName). Please update the description before merge so reviewers and future archaeologists aren’t misled. -
Nit:
setActiveModelcan be simplified now that lookup is O(1). ThecompositeKey/foundModel/modeltriad is leftover from the loop-based version:public void setActiveModel(String modelKey) { CopilotModel model = modelObservable.getValue().get(modelKey); if (model == null) { return; } UserPreference preference = getUserPreference(); preference.setChatModel(modelKey); CompletableFuture.runAsync(this::persistUserPreference); ensureRealm(() -> activeModelObservable.setValue(model)); }
-
FYI: No persistence migration is needed — the previous code already wrote the composite key (
compositeKey = entry.getKey()) toUserPreference.setChatModel, so existing preferences remain valid. Worth noting explicitly in the commit message / PR description so reviewers don't have to verify it themselves. -
Praise: Good instinct dropping the misleading comment
// setActiveModel will be called after models are loaded— both old and new code look up againstmodelObservable.getValue()at call time, so the comment was incorrect.
Checklist
- Simplify setActiveModel to direct O(1) map lookup, removing leftover loop-based boilerplate - Add TODO(microsoft#261) to findModelKeyByName documenting the same-name ambiguity in the custom-mode event path - Add ModelPickerGroupsBuilderTest asserting that two models sharing a modelName produce DropdownItems with distinct IDs (one per model key)
|
@ethanyhou thanks for the detailed review — addressed all points: Blocking:
Non-blocking: |
@ethanyhou echo this again, please help confirm this. |
|
@jdneo Thanks for the reminder, fallback is not required after TBB. |
|
Happy to remove the fallback model logic as part of this PR if that's preferred — just let me know. |
The fallback model path (getFallbackModel, setFallBackModelAsActiveModel, and the 402 replay logic in ChatContentViewer) was gated on !tokenBasedBillingEnabled(). Now that TBB has rolled out, this code is dead. Removing it as confirmed by @ethanyhou in PR microsoft#246.
|
Went ahead and removed the legacy TBB fallback logic — |
Problem
When a BYOK model and a native Copilot model share the same
modelName, the model picker dropdown usedgetModelName()as the item ID. This caused both models to map to the same dropdown item, so selecting one would always activate the other.Fix
Use
CopilotModel.getModelKey()(which returnsproviderName + "_" + idfor BYOK models, or justidfor native models) as the dropdown item ID. This is already the map key used throughoutModelService, so no new ID format is introduced.Changes
ModelPickerGroupsBuilder: Usemodel.getModelKey()as theDropdownItemID instead ofmodel.getModelName()ModelService.setActiveModel: Accepts a model key and does a direct O(1) map lookup (simplified from the previous loop-based approach)ModelService.bindModelPicker: UsesactiveModel.getModelKey()when syncing the picker selectionModelService.setFallBackModelAsActiveModel: PassesfallbackModel.getModelKey()tosetActiveModelModelService.findModelKeyByName: Used only by the custom-mode event handler (which receives a model name from the event payload). ATODO(#261)documents the residual same-name ambiguity in that path, which requires a protocol change to fix properly.Notes
UserPreference.setChatModel, so existing preferences remain valid.ModelPickerGroupsBuilderTest) asserts that two models sharing amodelNameproduceDropdownItems with distinct IDs.