diff --git a/src/api/providers/__tests__/vscode-lm-content-filtering.test.ts b/src/api/providers/__tests__/vscode-lm-content-filtering.test.ts new file mode 100644 index 00000000000..3f412ae7717 --- /dev/null +++ b/src/api/providers/__tests__/vscode-lm-content-filtering.test.ts @@ -0,0 +1,236 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import * as vscode from "vscode" +import { VsCodeLmHandler } from "../vscode-lm" + +// Mock vscode module +vi.mock("vscode", () => ({ + workspace: { + onDidChangeConfiguration: vi.fn(() => ({ dispose: vi.fn() })), + }, + lm: { + selectChatModels: vi.fn(), + }, + LanguageModelChatMessage: { + Assistant: vi.fn((content) => ({ role: "assistant", content })), + User: vi.fn((content) => ({ role: "user", content })), + }, + CancellationError: class extends Error { + constructor(message?: string) { + super(message) + this.name = "CancellationError" + } + }, + CancellationTokenSource: vi.fn(() => ({ + token: {}, + cancel: vi.fn(), + dispose: vi.fn(), + })), +})) + +describe("VsCodeLmHandler Content Filtering", () => { + let handler: VsCodeLmHandler + let mockClient: any + + beforeEach(() => { + vi.clearAllMocks() + + mockClient = { + id: "test-model", + name: "Test Model", + vendor: "test", + family: "test", + version: "1.0", + maxInputTokens: 8192, + sendRequest: vi.fn(), + countTokens: vi.fn().mockResolvedValue(10), + } + + vi.mocked(vscode.lm.selectChatModels).mockResolvedValue([mockClient]) + + handler = new VsCodeLmHandler({ + vsCodeLmModelSelector: { vendor: "test", family: "test" }, + }) + }) + + describe("Content Filtering Error Detection", () => { + it("should detect content filtering errors in Error objects", async () => { + const filteringError = new Error("Response was filtered by content policy") + mockClient.sendRequest.mockRejectedValue(filteringError) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + await expect(async () => { + const generator = handler.createMessage(systemPrompt, messages) + for await (const chunk of generator) { + // Should not reach here + } + }).rejects.toThrow(/Response was filtered by VS Code's content policy/) + }) + + it("should detect content filtering errors with 'inappropriate' keyword", async () => { + const filteringError = new Error("Content deemed inappropriate") + mockClient.sendRequest.mockRejectedValue(filteringError) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + await expect(async () => { + const generator = handler.createMessage(systemPrompt, messages) + for await (const chunk of generator) { + // Should not reach here + } + }).rejects.toThrow(/Response was filtered by VS Code's content policy/) + }) + + it("should detect content filtering errors with 'safety' keyword", async () => { + const filteringError = new Error("Safety violation detected") + mockClient.sendRequest.mockRejectedValue(filteringError) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + await expect(async () => { + const generator = handler.createMessage(systemPrompt, messages) + for await (const chunk of generator) { + // Should not reach here + } + }).rejects.toThrow(/Response was filtered by VS Code's content policy/) + }) + + it("should detect content filtering errors with 'blocked' keyword", async () => { + const filteringError = new Error("Request blocked by policy") + mockClient.sendRequest.mockRejectedValue(filteringError) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + await expect(async () => { + const generator = handler.createMessage(systemPrompt, messages) + for await (const chunk of generator) { + // Should not reach here + } + }).rejects.toThrow(/Response was filtered by VS Code's content policy/) + }) + + it("should detect content filtering errors in error objects", async () => { + const filteringError = { + message: "Content filtered", + code: "CONTENT_POLICY_VIOLATION", + } + mockClient.sendRequest.mockRejectedValue(filteringError) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + await expect(async () => { + const generator = handler.createMessage(systemPrompt, messages) + for await (const chunk of generator) { + // Should not reach here + } + }).rejects.toThrow(/Response was filtered by VS Code's content policy/) + }) + + it("should detect content filtering errors in error objects with reason property", async () => { + const filteringError = { + reason: "Content policy violation", + type: "FILTERING_ERROR", + } + mockClient.sendRequest.mockRejectedValue(filteringError) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + await expect(async () => { + const generator = handler.createMessage(systemPrompt, messages) + for await (const chunk of generator) { + // Should not reach here + } + }).rejects.toThrow(/Response was filtered by VS Code's content policy/) + }) + + it("should not detect content filtering for regular errors", async () => { + const regularError = new Error("Network timeout") + mockClient.sendRequest.mockRejectedValue(regularError) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + await expect(async () => { + const generator = handler.createMessage(systemPrompt, messages) + for await (const chunk of generator) { + // Should not reach here + } + }).rejects.toThrow("Network timeout") + }) + + it("should handle cancellation errors correctly", async () => { + const cancellationError = new vscode.CancellationError() + mockClient.sendRequest.mockRejectedValue(cancellationError) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + await expect(async () => { + const generator = handler.createMessage(systemPrompt, messages) + for await (const chunk of generator) { + // Should not reach here + } + }).rejects.toThrow(/Request cancelled by user/) + }) + + it("should provide helpful error message for content filtering", async () => { + const filteringError = new Error("Response got filtered FIX!!") + mockClient.sendRequest.mockRejectedValue(filteringError) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + try { + const generator = handler.createMessage(systemPrompt, messages) + for await (const chunk of generator) { + // Should not reach here + } + } catch (error) { + expect(error).toBeInstanceOf(Error) + const errorMessage = (error as Error).message + expect(errorMessage).toContain("Response was filtered by VS Code's content policy") + expect(errorMessage).toContain("Try rephrasing your request") + expect(errorMessage).toContain("Original error: Response got filtered FIX!!") + } + }) + }) + + describe("Case Insensitive Detection", () => { + it("should detect content filtering errors case-insensitively", async () => { + const filteringError = new Error("CONTENT POLICY VIOLATION") + mockClient.sendRequest.mockRejectedValue(filteringError) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + await expect(async () => { + const generator = handler.createMessage(systemPrompt, messages) + for await (const chunk of generator) { + // Should not reach here + } + }).rejects.toThrow(/Response was filtered by VS Code's content policy/) + }) + + it("should detect content filtering in error name case-insensitively", async () => { + const filteringError = new Error("Some error") + filteringError.name = "CONTENT_FILTERED_ERROR" + mockClient.sendRequest.mockRejectedValue(filteringError) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + await expect(async () => { + const generator = handler.createMessage(systemPrompt, messages) + for await (const chunk of generator) { + // Should not reach here + } + }).rejects.toThrow(/Response was filtered by VS Code's content policy/) + }) + }) +}) diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts index 6474371beeb..4ff748d28d7 100644 --- a/src/api/providers/vscode-lm.ts +++ b/src/api/providers/vscode-lm.ts @@ -287,6 +287,60 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan } } + private isContentFilteringError(error: Error): boolean { + const message = error.message.toLowerCase() + const name = error.name.toLowerCase() + + // Common patterns that indicate content filtering + const filteringPatterns = [ + "filtered", + "content policy", + "inappropriate", + "safety", + "blocked", + "restricted", + "content violation", + "policy violation", + "harmful content", + "unsafe content", + ] + + return filteringPatterns.some((pattern) => message.includes(pattern) || name.includes(pattern)) + } + + private isContentFilteringErrorObject(error: any): boolean { + if (!error || typeof error !== "object") { + return false + } + + // Check various properties that might indicate content filtering + const checkProperties = ["message", "reason", "code", "type", "error", "description"] + + for (const prop of checkProperties) { + if (error[prop] && typeof error[prop] === "string") { + const value = error[prop].toLowerCase() + const filteringPatterns = [ + "filtered", + "content policy", + "inappropriate", + "safety", + "blocked", + "restricted", + "content violation", + "policy violation", + "harmful content", + "unsafe content", + ] + + if (filteringPatterns.some((pattern) => value.includes(pattern))) { + return true + } + } + } + + return false + } + private async getClient(): Promise { if (!this.client) { console.debug("Roo Code : Getting client with options:", { @@ -467,12 +521,33 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan name: error.name, }) + // Check for content filtering errors + if (this.isContentFilteringError(error)) { + throw new Error( + "Roo Code : Response was filtered by VS Code's content policy. " + + "This may happen when the model's response contains content that VS Code considers inappropriate. " + + "Try rephrasing your request or using a different approach. " + + `Original error: ${error.message}`, + ) + } + // Return original error if it's already an Error instance throw error } else if (typeof error === "object" && error !== null) { // Handle error-like objects const errorDetails = JSON.stringify(error, null, 2) console.error("Roo Code : Stream error object:", errorDetails) + + // Check if the error object indicates content filtering + if (this.isContentFilteringErrorObject(error)) { + throw new Error( + "Roo Code : Response was filtered by VS Code's content policy. " + + "This may happen when the model's response contains content that VS Code considers inappropriate. " + + "Try rephrasing your request or using a different approach. " + + `Original error: ${errorDetails}`, + ) + } + throw new Error(`Roo Code : Response stream error: ${errorDetails}`) } else { // Fallback for unknown error types