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
19 changes: 19 additions & 0 deletions src/api/providers/__tests__/vscode-lm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,25 @@ describe("VsCodeLmHandler", () => {
})
})

it("should select model by ID when ID is provided", async () => {
const mockModel = { ...mockLanguageModelChat, id: "specific-model-id" }
// Mock returning multiple models when fetching all
;(vscode.lm.selectChatModels as Mock).mockResolvedValueOnce([
{ ...mockLanguageModelChat, id: "other-model" },
mockModel,
{ ...mockLanguageModelChat, id: "another-model" },
])

const client = await handler["createClient"]({
id: "specific-model-id",
})

expect(client).toBeDefined()
expect(client.id).toBe("specific-model-id")
// When selecting by ID, we fetch all models first
expect(vscode.lm.selectChatModels).toHaveBeenCalledWith({})
})

it("should return default client when no models available", async () => {
;(vscode.lm.selectChatModels as Mock).mockResolvedValueOnce([])

Expand Down
65 changes: 58 additions & 7 deletions src/api/providers/vscode-lm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,35 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
*/
async createClient(selector: vscode.LanguageModelChatSelector): Promise<vscode.LanguageModelChat> {
try {
// If we have an ID, try to find the specific model by ID first
if (selector.id) {
// Get all available models
const allModels = await vscode.lm.selectChatModels({})

// Find the model with the matching ID
const modelById = allModels.find((model) => model.id === selector.id)

if (modelById) {
console.debug(`Roo Code <Language Model API>: Found model by ID: ${modelById.id}`)
return modelById
} else {
console.warn(
`Roo Code <Language Model API>: Model with ID '${selector.id}' not found, falling back to selector`,
)
}
}

// Fallback to selector-based selection
const models = await vscode.lm.selectChatModels(selector)
Comment on lines +126 to 133
Copy link
Author

Choose a reason for hiding this comment

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

The fallback logic here doesn't work as intended. When a model with the specified ID is not found in allModels, the code attempts to fall back by calling selectChatModels(selector), but selector still contains the unfound ID. This means the fallback will return an empty array (since we already confirmed the ID doesn't exist), making the fallback ineffective. To fix this, create a new selector without the ID for the fallback: const { id, ...fallbackSelector } = selector and then use selectChatModels(fallbackSelector) to search by vendor/family only.


// Use first available model or create a minimal model object
if (models && Array.isArray(models) && models.length > 0) {
console.debug(`Roo Code <Language Model API>: Selected model: ${models[0].id}`)
return models[0]
}

// Create a minimal model if no models are available
console.warn(`Roo Code <Language Model API>: No models available, creating fallback model`)
return {
id: "default-lm",
name: "Default Language Model",
Expand Down Expand Up @@ -363,17 +384,38 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
try {
// Create the response stream with minimal required options
const requestOptions: vscode.LanguageModelChatRequestOptions = {
justification: `Roo Code would like to use '${client.name}' from '${client.vendor}', Click 'Allow' to proceed.`,
justification: `Roo Code would like to use '${client.name || client.id}' from '${client.vendor}'. Click 'Allow' to proceed.`,
}

// Note: Tool support is currently provided by the VSCode Language Model API directly
// Extensions can register tools using vscode.lm.registerTool()

const response: vscode.LanguageModelChatResponse = await client.sendRequest(
vsCodeLmMessages,
requestOptions,
this.currentRequestCancellation.token,
)
let response: vscode.LanguageModelChatResponse
try {
response = await client.sendRequest(
vsCodeLmMessages,
requestOptions,
this.currentRequestCancellation.token,
)
} catch (error) {
// Check if this is a model approval error
if (error instanceof Error) {
if (
error.message.includes("model_not_supported") ||
error.message.includes("Model is not supported")
) {
throw new Error(
"Model not approved. Please select the model in settings, then click 'Allow' when prompted by VS Code to approve access to the language model.",
)
} else if (error.message.includes("cancelled") || error.message.includes("Cancelled")) {
throw new Error(
"Model access was cancelled. Please approve access to use the VS Code Language Model API.",
)
}
}
// Re-throw the original error if it's not a known approval issue
throw error
}

// Consume the stream and handle both text and tool call chunks
for await (const chunk of response.stream) {
Expand Down Expand Up @@ -566,7 +608,16 @@ const VSCODE_LM_STATIC_BLACKLIST: string[] = ["claude-3.7-sonnet", "claude-3.7-s
export async function getVsCodeLmModels() {
try {
const models = (await vscode.lm.selectChatModels({})) || []
return models.filter((model) => !VSCODE_LM_STATIC_BLACKLIST.includes(model.id))
// Filter blacklisted models and ensure all required fields are present
return models
.filter((model) => !VSCODE_LM_STATIC_BLACKLIST.includes(model.id))
.map((model) => ({
id: model.id,
vendor: model.vendor,
family: model.family,
name: model.name,
version: model.version,
}))
} catch (error) {
console.error(
`Error fetching VS Code LM models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
Expand Down
22 changes: 12 additions & 10 deletions webview-ui/src/components/settings/providers/VSCodeLM.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,25 @@ export const VSCodeLM = ({ apiConfiguration, setApiConfigurationField }: VSCodeL
<label className="block font-medium mb-1">{t("settings:providers.vscodeLmModel")}</label>
{vsCodeLmModels.length > 0 ? (
<Select
value={
apiConfiguration?.vsCodeLmModelSelector
? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}`
: ""
}
value={apiConfiguration?.vsCodeLmModelSelector?.id || ""}
onValueChange={handleInputChange("vsCodeLmModelSelector", (value) => {
const [vendor, family] = value.split("/")
return { vendor, family }
// Find the selected model to get all its properties
const selectedModel = vsCodeLmModels.find((model) => model.id === value)
if (selectedModel) {
return {
id: selectedModel.id,
vendor: selectedModel.vendor,
family: selectedModel.family,
}
}
return { id: value }
})}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
{vsCodeLmModels.map((model) => (
<SelectItem
key={`${model.vendor}/${model.family}`}
value={`${model.vendor}/${model.family}`}>
<SelectItem key={model.id || `${model.vendor}/${model.family}`} value={model.id || ""}>
{`${model.vendor} - ${model.family}`}
</SelectItem>
))}
Expand Down