Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ModelID cannot be saved and refactor ModelPicker #1122

Merged
merged 4 commits into from
Feb 26, 2025

Conversation

System233
Copy link

@System233 System233 commented Feb 22, 2025

Description

The main changes for bug fixes are in f065f03

  • This commit fixes the event handling in the ModelPicker, allowing it to edit settings on the apiConfiguration provided by SettingsView, avoiding direct editing of ExtensionStateContext.
  • The events related to model list requests have been moved from ExtensionStateContext to be centrally handled in ApiOptions. The model list request events will be completely removed from ExtensionStateContext in a future commit to reduce the complexity of ExtensionStateContext.
  • The new ModelPicker uses a more streamlined auto-completion combobox, eliminating the need for tedious steps when entering custom models.

image

Commit 1a3b870 introduces the following user experience improvements:

  1. The form validation error message is now displayed directly below the ModelID input field, making it more noticeable for users. Previously, the error message appeared after the model list and model configuration, requiring users to scroll down to see it. Additionally, since it was displayed alongside the similarly colored "Note", users often mistook the error message for part of the note and overlooked it.

  2. When the form validation fails, an error message is shown on the Save button, and the button's border is highlighted in red (vscode-errorForeground) to indicate what the issue is.

  3. The validation messages for validateApiConfiguration and validateModelId have been consolidated. The functionality of these validations is very similar, and they can be merged in the future.

Now:

image
image

Before:

image image

Other notes:
The validation for validateApiConfiguration is incomplete, as not all providers correctly validate the key and modelId.

TODO:

  • Add validation to each input box

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Checklist:

  • My code follows the patterns of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation

Additional context

Related Issues

Reviewers


Important

Fix and refactor ModelPicker to improve snapshotting and add auto-complete feature.

  • Bug Fixes:
    • Fix ModelPicker not being properly snapshotted and remove references to ExtensionStateContext.
    • Correct type constraint for openAiCustomModelInfo in ApiOptions.tsx.
  • Refactoring:
    • Consolidate multiple model picker components into a single ModelPicker component in ModelPicker.tsx.
    • Remove OpenAiModelPicker, OpenRouterModelPicker, GlamaModelPicker, RequestyModelPicker, and UnboundModelPicker.
  • Features:
    • Add auto-complete feature to ModelPicker using Combobox in ModelPicker.tsx.
    • Simplify CustomModel input handling in ApiOptions.tsx.
  • Testing:
    • Update ModelPicker.test.tsx to test new auto-complete functionality and refactored ModelPicker behavior.

This description was created by Ellipsis for be47c8c. It will automatically update as commits are pushed.

Copy link

changeset-bot bot commented Feb 22, 2025

⚠️ No Changeset found

Latest commit: 4897500

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. bug Something isn't working labels Feb 22, 2025
@System233 System233 changed the title fix: ModelPicker not snapshotted and refactoring ModelPicker fix: ModelPicker not snapshotted and refactor ModelPicker Feb 22, 2025
@System233
Copy link
Author

@mrubens This issue prevents saving ModelID and should be investigated and fixed as soon as possible.
Related: #1168 #1151
May Related: #1115

@System233 System233 changed the title fix: ModelPicker not snapshotted and refactor ModelPicker fix: ModelID cannot be saved and refactor ModelPicker Feb 25, 2025
@mrubens
Copy link
Collaborator

mrubens commented Feb 25, 2025

@System233 thank you for the PR! There's a lot going on in here though, and I'm unsure about some of the UX changes. Would it be possible to separate out the bugfix into its own PR, or highlight which parts fix the bug so I can try? Thank you!

@System233
Copy link
Author

@System233 thank you for the PR! There's a lot going on in here though, and I'm unsure about some of the UX changes. Would it be possible to separate out the bugfix into its own PR, or highlight which parts fix the bug so I can try? Thank you!

I have split the commit.
The main changes for bug fixes are in f065f03

  • This commit fixes the event handling in the ModelPicker, allowing it to edit settings on the apiConfiguration provided by SettingsView, avoiding direct editing of ExtensionStateContext.
  • The events related to model list requests have been moved from ExtensionStateContext to be centrally handled in ApiOptions. The model list request events will be completely removed from ExtensionStateContext in a future commit to reduce the complexity of ExtensionStateContext.
  • The new ModelPicker uses a more streamlined auto-completion combobox, eliminating the need for tedious steps when entering custom models.

Comment on lines +160 to +212
switch (message.type) {
case "ollamaModels":
{
const newModels = message.ollamaModels ?? []
setOllamaModels(newModels)
}
break
case "lmStudioModels":
{
const newModels = message.lmStudioModels ?? []
setLmStudioModels(newModels)
}
break
case "vsCodeLmModels":
{
const newModels = message.vsCodeLmModels ?? []
setVsCodeLmModels(newModels)
}
break
case "glamaModels": {
const updatedModels = message.glamaModels ?? {}
setGlamaModels({
[glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model
...updatedModels,
})
break
}
case "openRouterModels": {
const updatedModels = message.openRouterModels ?? {}
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...updatedModels,
})
break
}
case "openAiModels": {
const updatedModels = message.openAiModels ?? []
setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults])))
break
}
case "unboundModels": {
const updatedModels = message.unboundModels ?? {}
setUnboundModels(updatedModels)
break
}
case "requestyModels": {
const updatedModels = message.requestyModels ?? {}
setRequestyModels({
[requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model
...updatedModels,
})
break
}
Copy link
Author

Choose a reason for hiding this comment

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

Handle all model list requests in ApiOptions

Comment on lines 706 to 721
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<OpenAiModelPicker />
<ModelPicker
apiConfiguration={apiConfiguration}
modelIdKey="openAiModelId"
modelInfoKey="openAiCustomModelInfo"
serviceName="OpenAI"
serviceUrl="https://platform.openai.com"
recommendedModel="gpt-4-turbo-preview"
models={openAiModels}
setApiConfigurationField={setApiConfigurationField}
defaultModelInfo={openAiModelInfoSaneDefaults}
errorMessage={errorMessage}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<Checkbox
Copy link
Author

Choose a reason for hiding this comment

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

Since provider-specific ModelPicker (such as OpenAiModelPicker) does not have substantial new code, all provider-specific ModelPickers are deleted here and ModelPicker is used directly.
In addition, in the previous OpenAiModelPicker, openAiCustomModelInfo was incorrectly mapped to openAiModelInfo due to incorrect type constraints.

Comment on lines -58 to -141
const [customModelId, setCustomModelId] = useState("")
const [isCustomModel, setIsCustomModel] = useState(false)
const [open, setOpen] = useState(false)
const [value, setValue] = useState(defaultModelId)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const prevRefreshValuesRef = useRef<Record<string, any> | undefined>()

const { apiConfiguration, [modelsKey]: models, onUpdateApiConfig, setApiConfiguration } = useExtensionState()

const modelIds = useMemo(
() => (Array.isArray(models) ? models : Object.keys(models)).sort((a, b) => a.localeCompare(b)),
[models],
)
const modelIds = useMemo(() => Object.keys(models ?? {}).sort((a, b) => a.localeCompare(b)), [models])

const { selectedModelId, selectedModelInfo } = useMemo(
() => normalizeApiConfiguration(apiConfiguration),
[apiConfiguration],
)

const onSelectCustomModel = useCallback(
(modelId: string) => {
setCustomModelId(modelId)
const modelInfo = { id: modelId }
const apiConfig = { ...apiConfiguration, [configKey]: modelId, [infoKey]: modelInfo }
setApiConfiguration(apiConfig)
onUpdateApiConfig(apiConfig)
setValue(modelId)
setOpen(false)
setIsCustomModel(false)
},
[apiConfiguration, configKey, infoKey, onUpdateApiConfig, setApiConfiguration],
)

const onSelect = useCallback(
(modelId: string) => {
const modelInfo = Array.isArray(models)
? { id: modelId } // For OpenAI models which are just strings
: models[modelId] // For other models that have full info objects
const apiConfig = { ...apiConfiguration, [configKey]: modelId, [infoKey]: modelInfo }
setApiConfiguration(apiConfig)
onUpdateApiConfig(apiConfig)
setValue(modelId)
setOpen(false)
const modelInfo = models?.[modelId]
setApiConfigurationField(modelIdKey, modelId)
setApiConfigurationField(modelInfoKey, modelInfo ?? defaultModelInfo)
},
[apiConfiguration, configKey, infoKey, models, onUpdateApiConfig, setApiConfiguration],
[modelIdKey, modelInfoKey, models, setApiConfigurationField, defaultModelInfo],
)

const debouncedRefreshModels = useMemo(() => {
return debounce(() => {
const message = refreshValues
? { type: refreshMessageType, values: refreshValues }
: { type: refreshMessageType }
vscode.postMessage(message)
}, 100)
}, [refreshMessageType, refreshValues])

useMount(() => {
debouncedRefreshModels()
return () => debouncedRefreshModels.clear()
})

useEffect(() => {
if (!refreshValues) {
prevRefreshValuesRef.current = undefined
return
}

// Check if all values in refreshValues are truthy
if (Object.values(refreshValues).some((value) => !value)) {
prevRefreshValuesRef.current = undefined
return
if (apiConfiguration[modelIdKey] == null && defaultModelId) {
onSelect(defaultModelId)
}

// Compare with previous values
const prevValues = prevRefreshValuesRef.current
if (prevValues && JSON.stringify(prevValues) === JSON.stringify(refreshValues)) {
return
}

prevRefreshValuesRef.current = refreshValues
debouncedRefreshModels()
}, [debouncedRefreshModels, refreshValues])

useEffect(() => setValue(selectedModelId), [selectedModelId])
Copy link
Author

Choose a reason for hiding this comment

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

const { apiConfiguration, [modelsKey]: models, onUpdateApiConfig, setApiConfiguration } = useExtensionState()

Previously, ModelPicker edited settings on the global ExtensionState/ExtensionStateContext, which prevented the modelID field from being saved.

The debounce and model list request related codes are useless here, and the custom model input function also introduces new UI errors, so I replaced it with an auto-complete combo box, and all problems were solved.
image

Comment on lines +89 to +109
<ComboboxItem key={model} value={model}>
{model}
</ComboboxItem>
))}
</ComboboxContent>
</Combobox>

{errorMessage ? (
<ApiErrorMessage errorMessage={errorMessage}>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
<span style={{ color: "var(--vscode-errorForeground)" }}>
<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
with Claude models. Less capable models may not work as expected.
</span>
</p>
</ApiErrorMessage>
Copy link
Author

Choose a reason for hiding this comment

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

Displaying the error message directly below the modelID input box makes it easier for users to see.

TODO: Do not display a default, built-in defaultModelInfo for unknown models

...updatedModels,
})
break
}
case "mcpServers": {
setMcpServers(message.mcpServers ?? [])
break
Copy link
Author

Choose a reason for hiding this comment

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

Further reduce the complexity of ExtensionStateContext, don’t stuff everything here.

@@ -0,0 +1,6 @@
import React from "react"
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should really figure out to get pure ESM modules to work with our jest configuration 🤔

Copy link
Author

Choose a reason for hiding this comment

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

I tried it before, try again now

Copy link
Author

@System233 System233 Feb 26, 2025

Choose a reason for hiding this comment

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

@cte
After trying it, adding the following configuration to jest can pass the test. Although it is better than mock export, I am not sure whether hard coding the cjs path is a good idea:

	moduleNameMapper: {
		"\\.(css|less|scss|sass)$": "identity-obj-proxy",
		"^vscrui$": "<rootDir>/src/__mocks__/vscrui.ts",
+  		"^lucide-react$": "<rootDir>/node_modules/lucide-react/dist/cjs/lucide-react.js",
		"^@vscode/webview-ui-toolkit/react$": "<rootDir>/src/__mocks__/@vscode/webview-ui-toolkit/react.ts",
		"^@/(.*)$": "<rootDir>/src/$1",
	},

Copy link
Collaborator

@cte cte left a comment

Choose a reason for hiding this comment

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

This is a huge improvement - thanks!

@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Feb 26, 2025
@cte cte merged commit 3860abf into RooVetGit:main Feb 26, 2025
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working lgtm This PR has been approved by a maintainer size:XXL This PR changes 1000+ lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants