feat: add OpenRouter quantization filter to exclude low-bit providers

Add a checkbox in OpenRouter settings to exclude low-bit quantization
providers (FP4/FP6/Int4) that cause CJK encoding issues. When enabled,
the provider.quantizations field is set to only allow FP8/FP16/BF16/Int8.

Closes #11325
This commit is contained in:
Roo Code 2026-02-10 06:03:36 +00:00
parent 5773af8ddd
commit 0bb4567201
5 changed files with 192 additions and 38 deletions

View file

@ -208,6 +208,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({

View file

@ -1166,4 +1166,136 @@ describe("OpenRouterHandler", () => {
)
})
})
describe("quantization filter", () => {
it("includes quantizations in providerOptions when openRouterExcludeLowQuantization is enabled", async () => {
const handler = new OpenRouterHandler({
openRouterApiKey: "test-key",
openRouterModelId: "openai/gpt-4o",
openRouterExcludeLowQuantization: true,
})
const mockFullStream = (async function* () {
yield { type: "text-delta", text: "test", id: "1" }
})()
mockStreamText.mockReturnValue({
fullStream: mockFullStream,
usage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
totalUsage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
})
const generator = handler.createMessage("test", [{ role: "user", content: "test" }])
for await (const _ of generator) {
// consume
}
expect(mockStreamText).toHaveBeenCalledWith(
expect.objectContaining({
providerOptions: {
openrouter: {
provider: {
quantizations: ["fp16", "bf16", "fp8", "int8"],
},
},
},
}),
)
})
it("does not include quantizations in providerOptions when openRouterExcludeLowQuantization is disabled", async () => {
const handler = new OpenRouterHandler({
openRouterApiKey: "test-key",
openRouterModelId: "openai/gpt-4o",
openRouterExcludeLowQuantization: false,
})
const mockFullStream = (async function* () {
yield { type: "text-delta", text: "test", id: "1" }
})()
mockStreamText.mockReturnValue({
fullStream: mockFullStream,
usage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
totalUsage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
})
const generator = handler.createMessage("test", [{ role: "user", content: "test" }])
for await (const _ of generator) {
// consume
}
expect(mockStreamText).toHaveBeenCalledWith(
expect.objectContaining({
providerOptions: undefined,
}),
)
})
it("combines quantizations with specific provider routing", async () => {
const handler = new OpenRouterHandler({
openRouterApiKey: "test-key",
openRouterModelId: "openai/gpt-4o",
openRouterExcludeLowQuantization: true,
openRouterSpecificProvider: "DeepInfra",
})
const mockFullStream = (async function* () {
yield { type: "text-delta", text: "test", id: "1" }
})()
mockStreamText.mockReturnValue({
fullStream: mockFullStream,
usage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
totalUsage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
})
const generator = handler.createMessage("test", [{ role: "user", content: "test" }])
for await (const _ of generator) {
// consume
}
expect(mockStreamText).toHaveBeenCalledWith(
expect.objectContaining({
providerOptions: {
openrouter: {
provider: {
order: ["DeepInfra"],
only: ["DeepInfra"],
allow_fallbacks: false,
quantizations: ["fp16", "bf16", "fp8", "int8"],
},
},
},
}),
)
})
it("includes quantizations in completePrompt when openRouterExcludeLowQuantization is enabled", async () => {
const handler = new OpenRouterHandler({
openRouterApiKey: "test-key",
openRouterModelId: "openai/gpt-4o",
openRouterExcludeLowQuantization: true,
})
mockGenerateText.mockResolvedValue({ text: "test" })
await handler.completePrompt("test prompt")
expect(mockGenerateText).toHaveBeenCalledWith(
expect.objectContaining({
providerOptions: {
openrouter: {
provider: {
quantizations: ["fp16", "bf16", "fp8", "int8"],
},
},
},
}),
)
})
})
})

View file

@ -155,25 +155,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
const tools = convertToolsForAiSdk(metadata?.tools)
const providerOptions:
| {
openrouter?: {
provider?: { order: string[]; only: string[]; allow_fallbacks: boolean }
}
}
| undefined =
this.options.openRouterSpecificProvider &&
this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME
? {
openrouter: {
provider: {
order: [this.options.openRouterSpecificProvider],
only: [this.options.openRouterSpecificProvider],
allow_fallbacks: false,
},
},
}
: undefined
const providerOptions = this.buildProviderOptions()
let accumulatedReasoningText = ""
@ -281,6 +263,47 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
return { id, info, topP: isDeepSeekR1 ? 0.95 : undefined, ...params }
}
private buildProviderOptions():
| {
openrouter?: {
provider?: {
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 { openrouter: { provider } }
}
async completePrompt(prompt: string): Promise<string> {
let { id: modelId, maxTokens, temperature, topP, reasoning } = await this.fetchModel()
@ -298,25 +321,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
const openrouter = this.createOpenRouterProvider({ reasoning, headers })
const providerOptions:
| {
openrouter?: {
provider?: { order: string[]; only: string[]; allow_fallbacks: boolean }
}
}
| undefined =
this.options.openRouterSpecificProvider &&
this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME
? {
openrouter: {
provider: {
order: [this.options.openRouterSpecificProvider],
only: [this.options.openRouterSpecificProvider],
allow_fallbacks: false,
},
},
}
: undefined
const providerOptions = this.buildProviderOptions()
try {
const result = await generateText({

View file

@ -103,6 +103,18 @@ export const OpenRouter = ({
)}
</div>
)}
<div>
<Checkbox
checked={apiConfiguration?.openRouterExcludeLowQuantization ?? false}
onChange={(checked: boolean) => {
setApiConfigurationField("openRouterExcludeLowQuantization", checked)
}}>
{t("settings:providers.openRouter.excludeLowQuantization.label")}
</Checkbox>
<div className="text-sm text-vscode-descriptionForeground mt-1 ml-6">
{t("settings:providers.openRouter.excludeLowQuantization.description")}
</div>
</div>
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}

View file

@ -550,6 +550,10 @@
"title": "OpenRouter Provider Routing",
"description": "OpenRouter routes requests to the best available providers for your model. By default, requests are load balanced across the top providers to maximize uptime. However, you can choose a specific provider to use for this model.",
"learnMore": "Learn more about provider routing"
},
"excludeLowQuantization": {
"label": "Exclude low-bit quantization (FP4/FP6/Int4)",
"description": "Only allow higher precision providers (FP8/FP16/BF16/Int8). Helps prevent broken CJK (Korean/Chinese/Japanese) encoding from aggressively quantized models."
}
},
"customModel": {