-
Notifications
You must be signed in to change notification settings - Fork 193
fix(ollama): always respond to model refresh, surface errors, and use form-edited base URL #878
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1203,23 +1203,38 @@ export const webviewMessageHandler = async ( | |||||
| case "requestOllamaModels": { | ||||||
| // Specific handler for Ollama models only. | ||||||
| const { apiConfiguration: ollamaApiConfig } = await provider.getState() | ||||||
| // Prefer the baseUrl/apiKey from the message values (which reflect | ||||||
| // the user's unsaved edits in the settings form) over the saved | ||||||
| // state, so the refresh uses the URL the user is actually looking | ||||||
| // at — not the stale one from before they started editing. | ||||||
| const baseUrl = message.values?.baseUrl ?? ollamaApiConfig.ollamaBaseUrl | ||||||
| const apiKey = message.values?.apiKey ?? ollamaApiConfig.ollamaApiKey | ||||||
| try { | ||||||
| const ollamaOptions = { | ||||||
| provider: "ollama" as const, | ||||||
| baseUrl: ollamaApiConfig.ollamaBaseUrl, | ||||||
| apiKey: ollamaApiConfig.ollamaApiKey, | ||||||
| baseUrl, | ||||||
| apiKey, | ||||||
| } | ||||||
| // Flush cache and refresh to ensure fresh models. | ||||||
| await flushModels(ollamaOptions, true) | ||||||
|
|
||||||
| const ollamaModels = await getModels(ollamaOptions) | ||||||
|
|
||||||
| if (Object.keys(ollamaModels).length > 0) { | ||||||
| provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: ollamaModels }) | ||||||
| } | ||||||
| // Always post a response so the webview refresh status can | ||||||
| // transition out of "loading" — even when no models are found. | ||||||
| provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: ollamaModels }) | ||||||
| } catch (error) { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both |
||||||
| // Silently fail - user hasn't configured Ollama yet | ||||||
| console.debug("Ollama models fetch failed:", error) | ||||||
| // Log the error to the output channel for debugging, but still | ||||||
| // post an empty response so the webview doesn't stay stuck in | ||||||
| // the loading state. Include the error message so the webview | ||||||
| // can display it directly in the settings panel. | ||||||
| const errorMsg = error instanceof Error ? error.message : String(error) | ||||||
| provider.log(`[requestOllamaModels] Failed to fetch models: ${errorMsg}`) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the user has typed a new URL but not saved, this log won’t show which host was actually attempted. Worth including
Suggested change
|
||||||
| provider.postMessageToWebview({ | ||||||
| type: "ollamaModels", | ||||||
| ollamaModels: {}, | ||||||
| error: errorMsg, | ||||||
| }) | ||||||
| } | ||||||
| break | ||||||
| } | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -218,7 +218,13 @@ const ApiOptions = ({ | |
| }, | ||
| }) | ||
| } else if (selectedProvider === "ollama") { | ||
| vscode.postMessage({ type: "requestOllamaModels" }) | ||
| vscode.postMessage({ | ||
| type: "requestOllamaModels", | ||
| values: { | ||
| baseUrl: apiConfiguration?.ollamaBaseUrl, | ||
| apiKey: apiConfiguration?.ollamaApiKey, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| }, | ||
| }) | ||
| } else if (selectedProvider === "lmstudio") { | ||
| requestLmStudioModels(apiConfiguration?.lmStudioBaseUrl) | ||
| } else if (selectedProvider === "vscode-lm") { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,12 +1,12 @@ | ||||||||||||||||||||||
| import { useState, useCallback, useMemo, useEffect } from "react" | ||||||||||||||||||||||
| import { useEvent } from "react-use" | ||||||||||||||||||||||
| import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" | ||||||||||||||||||||||
| import { Checkbox } from "vscrui" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import { type ProviderSettings, type ExtensionMessage, type ModelRecord, ollamaDefaultModelInfo } from "@roo-code/types" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import { useAppTranslation } from "@src/i18n/TranslationContext" | ||||||||||||||||||||||
| import { useRouterModels } from "@src/components/ui/hooks/useRouterModels" | ||||||||||||||||||||||
| import { Button } from "@src/components/ui" | ||||||||||||||||||||||
| import { vscode } from "@src/utils/vscode" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import { inputEventTransform } from "../transforms" | ||||||||||||||||||||||
|
|
@@ -22,6 +22,8 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro | |||||||||||||||||||||
| const { t } = useAppTranslation() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const [ollamaModels, setOllamaModels] = useState<ModelRecord>({}) | ||||||||||||||||||||||
| const [refreshStatus, setRefreshStatus] = useState<"idle" | "loading" | "success" | "error">("idle") | ||||||||||||||||||||||
| const [refreshError, setRefreshError] = useState<string | undefined>() | ||||||||||||||||||||||
| const routerModels = useRouterModels() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const handleInputChange = useCallback( | ||||||||||||||||||||||
|
|
@@ -35,20 +37,42 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro | |||||||||||||||||||||
| [setApiConfigurationField], | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const onMessage = useCallback((event: MessageEvent) => { | ||||||||||||||||||||||
| const message: ExtensionMessage = event.data | ||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||
| const handleMessage = (event: MessageEvent) => { | ||||||||||||||||||||||
| const message: ExtensionMessage = event.data | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (message.type === "ollamaModels") { | ||||||||||||||||||||||
| const newModels = message.ollamaModels ?? {} | ||||||||||||||||||||||
| setOllamaModels(newModels) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| switch (message.type) { | ||||||||||||||||||||||
| case "ollamaModels": | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| const newModels = message.ollamaModels ?? {} | ||||||||||||||||||||||
| setOllamaModels(newModels) | ||||||||||||||||||||||
| if (refreshStatus === "loading") { | ||||||||||||||||||||||
| if (Object.keys(newModels).length > 0) { | ||||||||||||||||||||||
| setRefreshStatus("success") | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| setRefreshStatus("error") | ||||||||||||||||||||||
|
Comment on lines
+49
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When Ollama is running but no models are installed, the backend posts
Suggested change
|
||||||||||||||||||||||
| setRefreshError(message.error) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| break | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }, []) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| useEvent("message", onMessage) | ||||||||||||||||||||||
| window.addEventListener("message", handleMessage) | ||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||
| window.removeEventListener("message", handleMessage) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }, [refreshStatus]) | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The A |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const handleRefreshModels = useCallback(() => { | ||||||||||||||||||||||
| setRefreshStatus("loading") | ||||||||||||||||||||||
| setRefreshError(undefined) | ||||||||||||||||||||||
| vscode.postMessage({ | ||||||||||||||||||||||
| type: "requestOllamaModels", | ||||||||||||||||||||||
| values: { | ||||||||||||||||||||||
| baseUrl: apiConfiguration?.ollamaBaseUrl, | ||||||||||||||||||||||
| apiKey: apiConfiguration?.ollamaApiKey, | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
| }, [apiConfiguration?.ollamaBaseUrl, apiConfiguration?.ollamaApiKey]) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Refresh models on mount | ||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||
|
|
@@ -102,6 +126,33 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro | |||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| </VSCodeTextField> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| <Button | ||||||||||||||||||||||
| variant="outline" | ||||||||||||||||||||||
| onClick={handleRefreshModels} | ||||||||||||||||||||||
| disabled={refreshStatus === "loading"} | ||||||||||||||||||||||
| className="w-full"> | ||||||||||||||||||||||
| <div className="flex items-center gap-2"> | ||||||||||||||||||||||
| {refreshStatus === "loading" ? ( | ||||||||||||||||||||||
| <span className="codicon codicon-loading codicon-modifier-spin" /> | ||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||
| <span className="codicon codicon-refresh" /> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| {t("settings:providers.refreshModels.label")} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| {refreshStatus === "loading" && ( | ||||||||||||||||||||||
| <div className="text-sm text-vscode-descriptionForeground"> | ||||||||||||||||||||||
| {t("settings:providers.refreshModels.loading")} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| {refreshStatus === "success" && ( | ||||||||||||||||||||||
| <div className="text-sm text-vscode-foreground">{t("settings:providers.refreshModels.success")}</div> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| {refreshStatus === "error" && ( | ||||||||||||||||||||||
| <div className="text-sm text-vscode-errorForeground"> | ||||||||||||||||||||||
| {refreshError || t("settings:providers.refreshModels.error")} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| <ModelPicker | ||||||||||||||||||||||
| apiConfiguration={apiConfiguration} | ||||||||||||||||||||||
| setApiConfigurationField={setApiConfigurationField} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mockGetModelsis asserted with the right options, butmockFlushModelsisn’t checked here. IfflushModelsused the stale saved-state URL whilegetModelsused the form-edited one, this test would still pass. Worth adding: