From 841011d96fcce12c877fd139d2187d6699fd05ca Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 24 Feb 2026 02:08:59 +0000 Subject: [PATCH] feat: add OpenRouter quantization filter to exclude low-bit providers Adds an opt-in checkbox to filter out low-bit quantization providers (FP4/FP6/Int4) when using OpenRouter, preventing broken CJK encoding. - Add openRouterExcludeLowQuantization to provider settings schema - Add buildProviderOptions() method to centralize provider options - Add UI checkbox in OpenRouter settings - Add i18n translations for all 18 locales - Add 6 tests for buildProviderOptions behavior Closes #11325 --- packages/types/src/provider-settings.ts | 1 + .../providers/__tests__/openrouter.spec.ts | 73 +++++++++++++++++++ src/api/providers/openrouter.ts | 61 +++++++++++----- .../settings/providers/OpenRouter.tsx | 12 +++ webview-ui/src/i18n/locales/ca/settings.json | 4 + webview-ui/src/i18n/locales/de/settings.json | 4 + webview-ui/src/i18n/locales/en/settings.json | 4 + webview-ui/src/i18n/locales/es/settings.json | 4 + webview-ui/src/i18n/locales/fr/settings.json | 4 + webview-ui/src/i18n/locales/hi/settings.json | 4 + webview-ui/src/i18n/locales/id/settings.json | 4 + webview-ui/src/i18n/locales/it/settings.json | 4 + webview-ui/src/i18n/locales/ja/settings.json | 4 + webview-ui/src/i18n/locales/ko/settings.json | 4 + webview-ui/src/i18n/locales/nl/settings.json | 4 + webview-ui/src/i18n/locales/pl/settings.json | 4 + .../src/i18n/locales/pt-BR/settings.json | 4 + webview-ui/src/i18n/locales/ru/settings.json | 4 + webview-ui/src/i18n/locales/tr/settings.json | 4 + webview-ui/src/i18n/locales/vi/settings.json | 4 + .../src/i18n/locales/zh-CN/settings.json | 4 + .../src/i18n/locales/zh-TW/settings.json | 4 + 22 files changed, 201 insertions(+), 18 deletions(-) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 859792d7c3..c38b4e75be 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -206,6 +206,7 @@ const openRouterSchema = baseProviderSettingsSchema.extend({ openRouterModelId: z.string().optional(), openRouterBaseUrl: z.string().optional(), openRouterSpecificProvider: z.string().optional(), + openRouterExcludeLowQuantization: z.boolean().optional(), }) const bedrockSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/__tests__/openrouter.spec.ts b/src/api/providers/__tests__/openrouter.spec.ts index e03abea635..6797ca588b 100644 --- a/src/api/providers/__tests__/openrouter.spec.ts +++ b/src/api/providers/__tests__/openrouter.spec.ts @@ -698,4 +698,77 @@ describe("OpenRouterHandler", () => { ) }) }) + + describe("buildProviderOptions", () => { + it("returns undefined when no specific provider and no quantization filter", () => { + const handler = new OpenRouterHandler({ + openRouterApiKey: "test-key", + openRouterModelId: "anthropic/claude-sonnet-4", + }) + const result = (handler as any).buildProviderOptions() + expect(result).toBeUndefined() + }) + + it("returns provider routing when specific provider is set", () => { + const handler = new OpenRouterHandler({ + openRouterApiKey: "test-key", + openRouterModelId: "anthropic/claude-sonnet-4", + openRouterSpecificProvider: "Anthropic", + }) + const result = (handler as any).buildProviderOptions() + expect(result).toEqual({ + order: ["Anthropic"], + only: ["Anthropic"], + allow_fallbacks: false, + }) + }) + + it("returns quantizations when excludeLowQuantization is enabled", () => { + const handler = new OpenRouterHandler({ + openRouterApiKey: "test-key", + openRouterModelId: "anthropic/claude-sonnet-4", + openRouterExcludeLowQuantization: true, + }) + const result = (handler as any).buildProviderOptions() + expect(result).toEqual({ + quantizations: ["fp16", "bf16", "fp8", "int8"], + }) + }) + + it("combines specific provider and quantization filter", () => { + const handler = new OpenRouterHandler({ + openRouterApiKey: "test-key", + openRouterModelId: "anthropic/claude-sonnet-4", + openRouterSpecificProvider: "Anthropic", + openRouterExcludeLowQuantization: true, + }) + const result = (handler as any).buildProviderOptions() + expect(result).toEqual({ + order: ["Anthropic"], + only: ["Anthropic"], + allow_fallbacks: false, + quantizations: ["fp16", "bf16", "fp8", "int8"], + }) + }) + + it("returns undefined when specific provider is the default", () => { + const handler = new OpenRouterHandler({ + openRouterApiKey: "test-key", + openRouterModelId: "anthropic/claude-sonnet-4", + openRouterSpecificProvider: "[default]", + }) + const result = (handler as any).buildProviderOptions() + expect(result).toBeUndefined() + }) + + it("returns undefined when excludeLowQuantization is false", () => { + const handler = new OpenRouterHandler({ + openRouterApiKey: "test-key", + openRouterModelId: "anthropic/claude-sonnet-4", + openRouterExcludeLowQuantization: false, + }) + const result = (handler as any).buildProviderOptions() + expect(result).toBeUndefined() + }) + }) }) diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 7fcc24b15f..cab2b5b7aa 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -317,15 +317,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH messages: openAiMessages, stream: true, stream_options: { include_usage: true }, - // Only include provider if openRouterSpecificProvider is not "[default]". - ...(this.options.openRouterSpecificProvider && - this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME && { - provider: { - order: [this.options.openRouterSpecificProvider], - only: [this.options.openRouterSpecificProvider], - allow_fallbacks: false, - }, - }), + ...(this.buildProviderOptions() && { provider: this.buildProviderOptions() }), ...(reasoning && { reasoning }), tools: this.convertToolsForOpenAI(metadata?.tools), tool_choice: metadata?.tool_choice, @@ -574,6 +566,47 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH return { id, info, topP: isDeepSeekR1 ? 0.95 : undefined, ...params } } + /** + * Build the `provider` options object for OpenRouter requests. + * Combines specific provider routing and quantization filtering. + */ + private buildProviderOptions(): + | { + order?: string[] + only?: string[] + allow_fallbacks?: boolean + quantizations?: string[] + } + | undefined { + const hasSpecificProvider = + this.options.openRouterSpecificProvider && + this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME + const excludeLowQuantization = this.options.openRouterExcludeLowQuantization + + if (!hasSpecificProvider && !excludeLowQuantization) { + return undefined + } + + const provider: { + order?: string[] + only?: string[] + allow_fallbacks?: boolean + quantizations?: string[] + } = {} + + if (hasSpecificProvider) { + provider.order = [this.options.openRouterSpecificProvider!] + provider.only = [this.options.openRouterSpecificProvider!] + provider.allow_fallbacks = false + } + + if (excludeLowQuantization) { + provider.quantizations = ["fp16", "bf16", "fp8", "int8"] + } + + return provider + } + async completePrompt(prompt: string) { let { id: modelId, maxTokens, temperature, reasoning } = await this.fetchModel() @@ -583,15 +616,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH temperature, messages: [{ role: "user", content: prompt }], stream: false, - // Only include provider if openRouterSpecificProvider is not "[default]". - ...(this.options.openRouterSpecificProvider && - this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME && { - provider: { - order: [this.options.openRouterSpecificProvider], - only: [this.options.openRouterSpecificProvider], - allow_fallbacks: false, - }, - }), + ...(this.buildProviderOptions() && { provider: this.buildProviderOptions() }), ...(reasoning && { reasoning }), } diff --git a/webview-ui/src/components/settings/providers/OpenRouter.tsx b/webview-ui/src/components/settings/providers/OpenRouter.tsx index 2dba8c8459..8ada9cc6bb 100644 --- a/webview-ui/src/components/settings/providers/OpenRouter.tsx +++ b/webview-ui/src/components/settings/providers/OpenRouter.tsx @@ -103,6 +103,18 @@ export const OpenRouter = ({ )} )} +
+ { + setApiConfigurationField("openRouterExcludeLowQuantization", checked) + }}> + {t("settings:providers.openRouter.excludeLowQuantization.label")} + +
+ {t("settings:providers.openRouter.excludeLowQuantization.description")} +
+