From 22a9316cf81adb486eb8cb640cd77002d28ad18e Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Wed, 4 Feb 2026 17:04:40 -0500 Subject: [PATCH 1/4] feat: implement Qwen provider with AI SDK Adds a new Qwen provider (distinct from Qwen Code) using the qwen-ai-provider-v5 package and AI SDK. Uses Alibaba Cloud's DashScope API. - Add QwenHandler using AI SDK with streamText/generateText - Add 14 Qwen models (qwen-max, qwen-plus, qwen-turbo, vision models, etc.) - Add UI settings component with API key field - Add 22 tests for the provider Models included: - qwen-max, qwen-max-latest - qwen-plus, qwen-plus-latest (default) - qwen-turbo, qwen-turbo-latest - qwen2.5-72b/32b/14b/7b-instruct - qwen2.5-14b-instruct-1m (1M context) - qwen3-235b-a22b - qwen-vl-max, qwen-vl-plus (vision) Closes EXT-712 --- packages/types/src/provider-settings.ts | 11 + packages/types/src/providers/index.ts | 4 + packages/types/src/providers/qwen.ts | 156 +++++++ pnpm-lock.yaml | 17 +- src/api/index.ts | 3 + src/api/providers/__tests__/qwen.spec.ts | 407 ++++++++++++++++++ src/api/providers/index.ts | 1 + src/api/providers/qwen.ts | 165 +++++++ src/package.json | 3 +- .../src/components/settings/ApiOptions.tsx | 11 + .../src/components/settings/constants.ts | 3 + .../components/settings/providers/Qwen.tsx | 51 +++ .../components/settings/providers/index.ts | 1 + .../components/ui/hooks/useSelectedModel.ts | 6 + webview-ui/src/i18n/locales/en/settings.json | 2 + 15 files changed, 839 insertions(+), 2 deletions(-) create mode 100644 packages/types/src/providers/qwen.ts create mode 100644 src/api/providers/__tests__/qwen.spec.ts create mode 100644 src/api/providers/qwen.ts create mode 100644 webview-ui/src/components/settings/providers/Qwen.tsx diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 0c5965f7ff6..a4ecb2cb8a5 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -19,6 +19,7 @@ import { openAiCodexModels, openAiNativeModels, qwenCodeModels, + qwenModels, sambaNovaModels, vertexModels, vscodeLlmModels, @@ -135,6 +136,7 @@ export const providerNames = [ "openai-codex", "openai-native", "qwen-code", + "qwen", "roo", "sambanova", "vertex", @@ -403,6 +405,11 @@ const qwenCodeSchema = apiModelIdProviderModelSchema.extend({ qwenCodeOauthPath: z.string().optional(), }) +const qwenSchema = apiModelIdProviderModelSchema.extend({ + qwenApiKey: z.string().optional(), + qwenBaseUrl: z.string().optional(), +}) + const rooSchema = apiModelIdProviderModelSchema.extend({ // Can use cloud authentication or provide an API key (cli). rooApiKey: z.string().optional(), @@ -456,6 +463,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv featherlessSchema.merge(z.object({ apiProvider: z.literal("featherless") })), ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })), qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })), + qwenSchema.merge(z.object({ apiProvider: z.literal("qwen") })), rooSchema.merge(z.object({ apiProvider: z.literal("roo") })), vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })), defaultSchema, @@ -497,6 +505,7 @@ export const providerSettingsSchema = z.object({ ...featherlessSchema.shape, ...ioIntelligenceSchema.shape, ...qwenCodeSchema.shape, + ...qwenSchema.shape, ...rooSchema.shape, ...vercelAiGatewaySchema.shape, ...codebaseIndexProviderSchema.shape, @@ -568,6 +577,7 @@ export const modelIdKeysByProvider: Record = { deepinfra: "deepInfraModelId", doubao: "apiModelId", "qwen-code": "apiModelId", + qwen: "apiModelId", unbound: "unboundModelId", requesty: "requestyModelId", xai: "apiModelId", @@ -691,6 +701,7 @@ export const MODELS_BY_PROVIDER: Record< models: Object.keys(openAiNativeModels), }, "qwen-code": { id: "qwen-code", label: "Qwen Code", models: Object.keys(qwenCodeModels) }, + qwen: { id: "qwen", label: "Qwen", models: Object.keys(qwenModels) }, roo: { id: "roo", label: "Roo Code Router", models: [] }, sambanova: { id: "sambanova", diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 2018954bbdd..716505e59d8 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -21,6 +21,7 @@ export * from "./openai-codex.js" export * from "./openai-codex-rate-limits.js" export * from "./openrouter.js" export * from "./qwen-code.js" +export * from "./qwen.js" export * from "./requesty.js" export * from "./roo.js" export * from "./sambanova.js" @@ -51,6 +52,7 @@ import { moonshotDefaultModelId } from "./moonshot.js" import { openAiCodexDefaultModelId } from "./openai-codex.js" import { openRouterDefaultModelId } from "./openrouter.js" import { qwenCodeDefaultModelId } from "./qwen-code.js" +import { qwenDefaultModelId } from "./qwen.js" import { requestyDefaultModelId } from "./requesty.js" import { rooDefaultModelId } from "./roo.js" import { sambaNovaDefaultModelId } from "./sambanova.js" @@ -140,6 +142,8 @@ export function getProviderDefaultModelId( return rooDefaultModelId case "qwen-code": return qwenCodeDefaultModelId + case "qwen": + return qwenDefaultModelId case "vercel-ai-gateway": return vercelAiGatewayDefaultModelId case "anthropic": diff --git a/packages/types/src/providers/qwen.ts b/packages/types/src/providers/qwen.ts new file mode 100644 index 00000000000..7238b903bea --- /dev/null +++ b/packages/types/src/providers/qwen.ts @@ -0,0 +1,156 @@ +import type { ModelInfo } from "../model.js" + +export type QwenModelId = + | "qwen-max" + | "qwen-max-latest" + | "qwen-plus" + | "qwen-plus-latest" + | "qwen-turbo" + | "qwen-turbo-latest" + | "qwen2.5-72b-instruct" + | "qwen2.5-32b-instruct" + | "qwen2.5-14b-instruct" + | "qwen2.5-7b-instruct" + | "qwen2.5-14b-instruct-1m" + | "qwen3-235b-a22b" + | "qwen-vl-max" + | "qwen-vl-plus" + +export const qwenDefaultModelId: QwenModelId = "qwen-plus" + +export const qwenModels = { + "qwen-max": { + maxTokens: 8_192, + contextWindow: 32_768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 2.0, // $2 per million tokens + outputPrice: 6.0, // $6 per million tokens + description: "Qwen Max - Most capable model, best for complex reasoning and generation tasks", + }, + "qwen-max-latest": { + maxTokens: 8_192, + contextWindow: 32_768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 2.0, + outputPrice: 6.0, + description: "Qwen Max Latest - Latest version of Qwen Max with most recent improvements", + }, + "qwen-plus": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.4, // $0.40 per million tokens + outputPrice: 1.2, // $1.20 per million tokens + description: "Qwen Plus - Balanced performance and cost for general tasks", + }, + "qwen-plus-latest": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.4, + outputPrice: 1.2, + description: "Qwen Plus Latest - Latest version of Qwen Plus", + }, + "qwen-turbo": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.06, // $0.06 per million tokens + outputPrice: 0.24, // $0.24 per million tokens + description: "Qwen Turbo - Fastest and most cost-effective for simple tasks", + }, + "qwen-turbo-latest": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.06, + outputPrice: 0.24, + description: "Qwen Turbo Latest - Latest version of Qwen Turbo", + }, + "qwen2.5-72b-instruct": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.8, // $0.80 per million tokens + outputPrice: 2.4, // $2.40 per million tokens + description: "Qwen 2.5 72B Instruct - Large open model with strong performance", + }, + "qwen2.5-32b-instruct": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.4, // $0.40 per million tokens + outputPrice: 1.2, // $1.20 per million tokens + description: "Qwen 2.5 32B Instruct - Medium-sized model with good balance", + }, + "qwen2.5-14b-instruct": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.28, // $0.28 per million tokens + outputPrice: 0.84, // $0.84 per million tokens + description: "Qwen 2.5 14B Instruct - Efficient model for various tasks", + }, + "qwen2.5-7b-instruct": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.14, // $0.14 per million tokens + outputPrice: 0.42, // $0.42 per million tokens + description: "Qwen 2.5 7B Instruct - Lightweight model for fast inference", + }, + "qwen2.5-14b-instruct-1m": { + maxTokens: 8_192, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.28, + outputPrice: 0.84, + description: "Qwen 2.5 14B Instruct 1M - Extended 1M context window variant", + }, + "qwen3-235b-a22b": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.8, // $0.80 per million tokens + outputPrice: 2.4, // $2.40 per million tokens + description: "Qwen 3 235B A22B - Largest Qwen 3 model with 235B parameters in MoE architecture", + }, + "qwen-vl-max": { + maxTokens: 8_192, + contextWindow: 32_768, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 3.0, // $3 per million tokens + outputPrice: 9.0, // $9 per million tokens + description: "Qwen VL Max - Best vision model, supports images and video understanding", + }, + "qwen-vl-plus": { + maxTokens: 8_192, + contextWindow: 32_768, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 1.2, // $1.20 per million tokens + outputPrice: 3.6, // $3.60 per million tokens + description: "Qwen VL Plus - Balanced vision model for image understanding tasks", + }, +} as const satisfies Record + +export const qwenDefaultModelInfo: ModelInfo = qwenModels[qwenDefaultModelId] + +// International endpoint (default) +export const QWEN_API_BASE_URL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" + +// China domestic endpoint +export const QWEN_API_BASE_URL_CHINA = "https://dashscope.aliyuncs.com/compatible-mode/v1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 417e69a07ad..9ebff4a0b1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -935,6 +935,9 @@ importers: puppeteer-core: specifier: ^23.4.0 version: 23.11.1 + qwen-ai-provider-v5: + specifier: ^2.1.0 + version: 2.1.0(zod@3.25.76) reconnecting-eventsource: specifier: ^1.6.4 version: 1.6.4 @@ -9155,6 +9158,12 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + qwen-ai-provider-v5@2.1.0: + resolution: {integrity: sha512-I+Iv45ymrez1wieZFu0n/lc/lSkbAQMlujWBCfUWBUOf6DizYfvPKaydsojXM7CU8TcqJbYJuN3ofnaxFIwBZA==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: 3.25.76 + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -15202,7 +15211,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -20167,6 +20176,12 @@ snapshots: queue-microtask@1.2.3: {} + qwen-ai-provider-v5@2.1.0(zod@3.25.76): + dependencies: + '@ai-sdk/provider': 3.0.7 + '@ai-sdk/provider-utils': 4.0.13(zod@3.25.76) + zod: 3.25.76 + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 diff --git a/src/api/index.ts b/src/api/index.ts index 30119b7dc7e..8f471a0466e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -30,6 +30,7 @@ import { ChutesHandler, LiteLLMHandler, QwenCodeHandler, + QwenHandler, SambaNovaHandler, IOIntelligenceHandler, DoubaoHandler, @@ -151,6 +152,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new DoubaoHandler(options) case "qwen-code": return new QwenCodeHandler(options) + case "qwen": + return new QwenHandler(options) case "moonshot": return new MoonshotHandler(options) case "vscode-lm": diff --git a/src/api/providers/__tests__/qwen.spec.ts b/src/api/providers/__tests__/qwen.spec.ts new file mode 100644 index 00000000000..543b4396aa0 --- /dev/null +++ b/src/api/providers/__tests__/qwen.spec.ts @@ -0,0 +1,407 @@ +// Use vi.hoisted to define mock functions that can be referenced in hoisted vi.mock() calls +const { mockStreamText, mockGenerateText } = vi.hoisted(() => ({ + mockStreamText: vi.fn(), + mockGenerateText: vi.fn(), +})) + +vi.mock("ai", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + streamText: mockStreamText, + generateText: mockGenerateText, + } +}) + +vi.mock("qwen-ai-provider-v5", () => ({ + createQwen: vi.fn(() => { + // Return a function that returns a mock language model + return vi.fn(() => ({ + modelId: "qwen-plus", + provider: "qwen", + })) + }), +})) + +import type { Anthropic } from "@anthropic-ai/sdk" + +import { qwenDefaultModelId } from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../../shared/api" + +import { QwenHandler } from "../qwen" + +describe("QwenHandler", () => { + let handler: QwenHandler + let mockOptions: ApiHandlerOptions + + beforeEach(() => { + mockOptions = { + qwenApiKey: "test-api-key", + apiModelId: "qwen-plus", + } + handler = new QwenHandler(mockOptions) + vi.clearAllMocks() + }) + + describe("constructor", () => { + it("should initialize with provided options", () => { + expect(handler).toBeInstanceOf(QwenHandler) + expect(handler.getModel().id).toBe(mockOptions.apiModelId) + }) + + it("should use default model ID if not provided", () => { + const handlerWithoutModel = new QwenHandler({ + ...mockOptions, + apiModelId: undefined, + }) + expect(handlerWithoutModel.getModel().id).toBe(qwenDefaultModelId) + }) + + it("should use default base URL if not provided", () => { + const handlerWithoutBaseUrl = new QwenHandler({ + ...mockOptions, + qwenBaseUrl: undefined, + }) + expect(handlerWithoutBaseUrl).toBeInstanceOf(QwenHandler) + }) + + it("should use custom base URL if provided", () => { + const customBaseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1" + const handlerWithCustomUrl = new QwenHandler({ + ...mockOptions, + qwenBaseUrl: customBaseUrl, + }) + expect(handlerWithCustomUrl).toBeInstanceOf(QwenHandler) + }) + }) + + describe("getModel", () => { + it("should return model info for valid model ID", () => { + const model = handler.getModel() + expect(model.id).toBe(mockOptions.apiModelId) + expect(model.info).toBeDefined() + expect(model.info.maxTokens).toBe(8_192) // qwen-plus has 8K max output + expect(model.info.contextWindow).toBe(131_072) + expect(model.info.supportsImages).toBe(false) + expect(model.info.supportsPromptCache).toBe(false) + }) + + it("should return correct model info for qwen-max", () => { + const handlerWithMax = new QwenHandler({ + ...mockOptions, + apiModelId: "qwen-max", + }) + const model = handlerWithMax.getModel() + expect(model.id).toBe("qwen-max") + expect(model.info).toBeDefined() + expect(model.info.maxTokens).toBe(8_192) + expect(model.info.contextWindow).toBe(32_768) + expect(model.info.supportsImages).toBe(false) + }) + + it("should return correct model info for qwen-vl-max (vision model)", () => { + const handlerWithVision = new QwenHandler({ + ...mockOptions, + apiModelId: "qwen-vl-max", + }) + const model = handlerWithVision.getModel() + expect(model.id).toBe("qwen-vl-max") + expect(model.info).toBeDefined() + expect(model.info.supportsImages).toBe(true) + }) + + it("should return provided model ID with default model info if model does not exist", () => { + const handlerWithInvalidModel = new QwenHandler({ + ...mockOptions, + apiModelId: "invalid-model", + }) + const model = handlerWithInvalidModel.getModel() + expect(model.id).toBe("invalid-model") // Returns provided ID + expect(model.info).toBeDefined() + // With the current implementation, it's the same object reference when using default model info + expect(model.info).toBe(handler.getModel().info) + }) + + it("should return default model if no model ID is provided", () => { + const handlerWithoutModel = new QwenHandler({ + ...mockOptions, + apiModelId: undefined, + }) + const model = handlerWithoutModel.getModel() + expect(model.id).toBe(qwenDefaultModelId) + expect(model.info).toBeDefined() + }) + + it("should include model parameters from getModelParams", () => { + const model = handler.getModel() + expect(model).toHaveProperty("temperature") + expect(model).toHaveProperty("maxTokens") + }) + }) + + describe("createMessage", () => { + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text" as const, + text: "Hello!", + }, + ], + }, + ] + + it("should handle streaming responses", async () => { + // Mock the fullStream async generator + async function* mockFullStream() { + yield { type: "text-delta", text: "Test response" } + } + + // Mock usage promise + const mockUsage = Promise.resolve({ + inputTokens: 10, + outputTokens: 5, + }) + + mockStreamText.mockReturnValue({ + fullStream: mockFullStream(), + usage: mockUsage, + providerMetadata: Promise.resolve({}), + }) + + const stream = handler.createMessage(systemPrompt, messages) + const results: unknown[] = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + expect(mockStreamText).toHaveBeenCalled() + expect(results.length).toBeGreaterThan(0) + }) + + it("should pass correct options to streamText", async () => { + async function* mockFullStream() { + yield { type: "text-delta", text: "Test" } + } + + mockStreamText.mockReturnValue({ + fullStream: mockFullStream(), + usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }), + providerMetadata: Promise.resolve({}), + }) + + const stream = handler.createMessage(systemPrompt, messages) + // Consume the stream + for await (const _ of stream) { + // Just consume + } + + expect(mockStreamText).toHaveBeenCalledWith( + expect.objectContaining({ + model: expect.any(Object), + system: systemPrompt, + messages: expect.any(Array), + temperature: expect.any(Number), + }), + ) + }) + + it("should yield text chunks from the stream", async () => { + async function* mockFullStream() { + yield { type: "text-delta", text: "Hello " } + yield { type: "text-delta", text: "World" } + } + + mockStreamText.mockReturnValue({ + fullStream: mockFullStream(), + usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }), + providerMetadata: Promise.resolve({}), + }) + + const stream = handler.createMessage(systemPrompt, messages) + const textChunks: string[] = [] + + for await (const chunk of stream) { + if (chunk && typeof chunk === "object" && "type" in chunk && chunk.type === "text") { + textChunks.push((chunk as { type: "text"; text: string }).text) + } + } + + expect(textChunks).toContain("Hello ") + expect(textChunks).toContain("World") + }) + + it("should yield usage chunk at the end", async () => { + async function* mockFullStream() { + yield { type: "text-delta", text: "Test" } + } + + const expectedUsage = { + inputTokens: 100, + outputTokens: 50, + } + + mockStreamText.mockReturnValue({ + fullStream: mockFullStream(), + usage: Promise.resolve(expectedUsage), + providerMetadata: Promise.resolve({}), + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: unknown[] = [] + + for await (const chunk of stream) { + chunks.push(chunk) + } + + const usageChunk = chunks.find((c: any) => c?.type === "usage") + expect(usageChunk).toBeDefined() + expect(usageChunk).toMatchObject({ + type: "usage", + inputTokens: 100, + outputTokens: 50, + }) + }) + + it("should handle tool calls via tool-input events", async () => { + async function* mockFullStream() { + yield { type: "text-delta", text: "Let me help you with that." } + yield { type: "tool-input-start", id: "call_123", toolName: "read_file" } + yield { type: "tool-input-delta", id: "call_123", delta: '{"path":"/test/file.txt"}' } + yield { type: "tool-input-end", id: "call_123" } + } + + mockStreamText.mockReturnValue({ + fullStream: mockFullStream(), + usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }), + providerMetadata: Promise.resolve({}), + }) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks: unknown[] = [] + + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Should have text chunks + expect(chunks.some((c: any) => c?.type === "text")).toBe(true) + // Should have tool call start, delta, and end chunks + expect(chunks.some((c: any) => c?.type === "tool_call_start")).toBe(true) + expect(chunks.some((c: any) => c?.type === "tool_call_delta")).toBe(true) + expect(chunks.some((c: any) => c?.type === "tool_call_end")).toBe(true) + }) + + it("should handle errors gracefully", async () => { + const testError = new Error("API Error") + ;(testError as any).message = "API Error" + + // eslint-disable-next-line require-yield + async function* mockFullStream() { + throw testError + } + + mockStreamText.mockReturnValue({ + fullStream: mockFullStream(), + usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }), + providerMetadata: Promise.resolve({}), + }) + + const stream = handler.createMessage(systemPrompt, messages) + + await expect(async () => { + for await (const _ of stream) { + // Consume the stream + } + }).rejects.toThrow() + }) + }) + + describe("completePrompt", () => { + it("should return text from generateText", async () => { + const expectedText = "This is a test response" + + mockGenerateText.mockResolvedValue({ + text: expectedText, + usage: { inputTokens: 10, outputTokens: 5 }, + }) + + const result = await handler.completePrompt("Test prompt") + + expect(result).toBe(expectedText) + expect(mockGenerateText).toHaveBeenCalledWith( + expect.objectContaining({ + model: expect.any(Object), + prompt: "Test prompt", + }), + ) + }) + + it("should pass temperature to generateText", async () => { + mockGenerateText.mockResolvedValue({ + text: "Response", + usage: { inputTokens: 0, outputTokens: 0 }, + }) + + await handler.completePrompt("Test prompt") + + expect(mockGenerateText).toHaveBeenCalledWith( + expect.objectContaining({ + temperature: expect.any(Number), + }), + ) + }) + + it("should pass maxOutputTokens to generateText", async () => { + mockGenerateText.mockResolvedValue({ + text: "Response", + usage: { inputTokens: 0, outputTokens: 0 }, + }) + + await handler.completePrompt("Test prompt") + + expect(mockGenerateText).toHaveBeenCalledWith( + expect.objectContaining({ + maxOutputTokens: expect.any(Number), + }), + ) + }) + }) + + describe("model variants", () => { + it("should handle qwen-turbo model", () => { + const turboHandler = new QwenHandler({ + ...mockOptions, + apiModelId: "qwen-turbo", + }) + const model = turboHandler.getModel() + expect(model.id).toBe("qwen-turbo") + expect(model.info.maxTokens).toBe(8_192) + expect(model.info.contextWindow).toBe(131_072) + }) + + it("should handle qwen2.5-72b-instruct model", () => { + const modelHandler = new QwenHandler({ + ...mockOptions, + apiModelId: "qwen2.5-72b-instruct", + }) + const model = modelHandler.getModel() + expect(model.id).toBe("qwen2.5-72b-instruct") + expect(model.info.maxTokens).toBe(8_192) + }) + + it("should handle qwen2.5-14b-instruct-1m model with 1M context", () => { + const modelHandler = new QwenHandler({ + ...mockOptions, + apiModelId: "qwen2.5-14b-instruct-1m", + }) + const model = modelHandler.getModel() + expect(model.id).toBe("qwen2.5-14b-instruct-1m") + expect(model.info.contextWindow).toBe(1_000_000) + }) + }) +}) diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index cf49f75f189..019f14818f3 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -21,6 +21,7 @@ export { OpenAICompatibleHandler } from "./openai-compatible" export type { OpenAICompatibleConfig } from "./openai-compatible" export { OpenRouterHandler } from "./openrouter" export { QwenCodeHandler } from "./qwen-code" +export { QwenHandler } from "./qwen" export { RequestyHandler } from "./requesty" export { SambaNovaHandler } from "./sambanova" export { UnboundHandler } from "./unbound" diff --git a/src/api/providers/qwen.ts b/src/api/providers/qwen.ts new file mode 100644 index 00000000000..416d1fa9cf0 --- /dev/null +++ b/src/api/providers/qwen.ts @@ -0,0 +1,165 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import { createQwen } from "qwen-ai-provider-v5" +import { streamText, generateText, ToolSet } from "ai" + +import { + qwenModels, + qwenDefaultModelId, + QWEN_API_BASE_URL, + QWEN_API_BASE_URL_CHINA, + type ModelInfo, +} from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" + +import { + convertToAiSdkMessages, + convertToolsForAiSdk, + processAiSdkStreamPart, + mapToolChoice, + handleAiSdkError, +} from "../transform/ai-sdk" +import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" +import { getModelParams } from "../transform/model-params" + +import { DEFAULT_HEADERS } from "./constants" +import { BaseProvider } from "./base-provider" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" + +export const QWEN_DEFAULT_TEMPERATURE = 0 + +/** + * Qwen provider using the qwen-ai-provider-v5 AI SDK package. + * Provides access to Alibaba Cloud's Qwen models via DashScope platform. + */ +export class QwenHandler extends BaseProvider implements SingleCompletionHandler { + protected options: ApiHandlerOptions + protected provider: ReturnType + + constructor(options: ApiHandlerOptions) { + super() + this.options = options + + // Determine base URL - use China endpoint if specified + const baseURL = options.qwenBaseUrl ?? QWEN_API_BASE_URL + + // Create the Qwen provider using AI SDK + this.provider = createQwen({ + baseURL, + apiKey: options.qwenApiKey ?? "not-provided", + headers: DEFAULT_HEADERS, + }) + } + + override getModel(): { id: string; info: ModelInfo; maxTokens?: number; temperature?: number } { + const id = this.options.apiModelId ?? qwenDefaultModelId + const info = qwenModels[id as keyof typeof qwenModels] || qwenModels[qwenDefaultModelId] + const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options }) + return { id, info, ...params } + } + + /** + * Get the language model for the configured model ID. + */ + protected getLanguageModel() { + const { id } = this.getModel() + return this.provider(id) + } + + /** + * Process usage metrics from the AI SDK response. + * Qwen provides standard usage metrics. + */ + protected processUsageMetrics(usage: { + inputTokens?: number + outputTokens?: number + details?: { + cachedInputTokens?: number + reasoningTokens?: number + } + }): ApiStreamUsageChunk { + return { + type: "usage", + inputTokens: usage.inputTokens || 0, + outputTokens: usage.outputTokens || 0, + cacheReadTokens: usage.details?.cachedInputTokens, + reasoningTokens: usage.details?.reasoningTokens, + } + } + + /** + * Get the max tokens parameter to include in the request. + */ + protected getMaxOutputTokens(): number | undefined { + const { info } = this.getModel() + return this.options.modelMaxTokens || info.maxTokens || undefined + } + + /** + * Create a message stream using the AI SDK. + */ + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const { temperature } = this.getModel() + const languageModel = this.getLanguageModel() + + // Convert messages to AI SDK format + const aiSdkMessages = convertToAiSdkMessages(messages) + + // Convert tools to OpenAI format first, then to AI SDK format + const openAiTools = this.convertToolsForOpenAI(metadata?.tools) + const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined + + // Build the request options + const requestOptions: Parameters[0] = { + model: languageModel, + system: systemPrompt, + messages: aiSdkMessages, + temperature: this.options.modelTemperature ?? temperature ?? QWEN_DEFAULT_TEMPERATURE, + maxOutputTokens: this.getMaxOutputTokens(), + tools: aiSdkTools, + toolChoice: mapToolChoice(metadata?.tool_choice), + } + + // Use streamText for streaming responses + const result = streamText(requestOptions) + + try { + // Process the full stream to get all events + for await (const part of result.fullStream) { + for (const chunk of processAiSdkStreamPart(part)) { + yield chunk + } + } + + // Yield usage metrics at the end + const usage = await result.usage + if (usage) { + yield this.processUsageMetrics(usage) + } + } catch (error) { + // Handle AI SDK errors (AI_RetryError, AI_APICallError, etc.) + throw handleAiSdkError(error, "Qwen") + } + } + + /** + * Complete a prompt using the AI SDK generateText. + */ + async completePrompt(prompt: string): Promise { + const { temperature } = this.getModel() + const languageModel = this.getLanguageModel() + + const { text } = await generateText({ + model: languageModel, + prompt, + maxOutputTokens: this.getMaxOutputTokens(), + temperature: this.options.modelTemperature ?? temperature ?? QWEN_DEFAULT_TEMPERATURE, + }) + + return text + } +} diff --git a/src/package.json b/src/package.json index 423463bf0ba..55d5fabb3ec 100644 --- a/src/package.json +++ b/src/package.json @@ -456,7 +456,6 @@ "@ai-sdk/groq": "^3.0.19", "@ai-sdk/mistral": "^3.0.0", "@ai-sdk/xai": "^3.0.46", - "sambanova-ai-provider": "^1.2.2", "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.37.0", "@anthropic-ai/vertex-sdk": "^0.7.0", @@ -514,8 +513,10 @@ "ps-tree": "^1.2.0", "puppeteer-chromium-resolver": "^24.0.0", "puppeteer-core": "^23.4.0", + "qwen-ai-provider-v5": "^2.1.0", "reconnecting-eventsource": "^1.6.4", "safe-stable-stringify": "^2.5.0", + "sambanova-ai-provider": "^1.2.2", "sanitize-filename": "^1.6.3", "say": "^0.16.0", "semver-compare": "^1.0.0", diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 939d2734d4b..4e51972099d 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -17,6 +17,7 @@ import { anthropicDefaultModelId, doubaoDefaultModelId, qwenCodeDefaultModelId, + qwenDefaultModelId, geminiDefaultModelId, deepSeekDefaultModelId, moonshotDefaultModelId, @@ -93,6 +94,7 @@ import { OpenAICodex, OpenRouter, QwenCode, + Qwen, Requesty, Roo, SambaNova, @@ -345,6 +347,7 @@ const ApiOptions = ({ cerebras: { field: "apiModelId", default: cerebrasDefaultModelId }, "openai-codex": { field: "apiModelId", default: openAiCodexDefaultModelId }, "qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId }, + qwen: { field: "apiModelId", default: qwenDefaultModelId }, "openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId }, gemini: { field: "apiModelId", default: geminiDefaultModelId }, deepseek: { field: "apiModelId", default: deepSeekDefaultModelId }, @@ -654,6 +657,14 @@ const ApiOptions = ({ /> )} + {selectedProvider === "qwen" && ( + + )} + {selectedProvider === "moonshot" && ( void + simplifySettings?: boolean +} + +export const Qwen = ({ apiConfiguration, setApiConfigurationField }: QwenProps) => { + const { t } = useAppTranslation() + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + return ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.qwenApiKey && ( + + {t("settings:providers.getQwenApiKey")} + + )} + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index bca620d052d..c7adb770483 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -17,6 +17,7 @@ export { OpenAICodex } from "./OpenAICodex" export { OpenAICompatible } from "./OpenAICompatible" export { OpenRouter } from "./OpenRouter" export { QwenCode } from "./QwenCode" +export { Qwen } from "./Qwen" export { Roo } from "./Roo" export { Requesty } from "./Requesty" export { SambaNova } from "./SambaNova" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 8eac6fa7403..c2bfdc18b71 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -29,6 +29,7 @@ import { ioIntelligenceModels, basetenModels, qwenCodeModels, + qwenModels, litellmDefaultModelInfo, lMStudioDefaultModelInfo, BEDROCK_1M_CONTEXT_MODEL_IDS, @@ -354,6 +355,11 @@ function getSelectedModel({ const info = qwenCodeModels[id as keyof typeof qwenCodeModels] return { id, info } } + case "qwen": { + const id = apiConfiguration.apiModelId ?? defaultModelId + const info = qwenModels[id as keyof typeof qwenModels] + return { id, info } + } case "openai-codex": { const id = apiConfiguration.apiModelId ?? defaultModelId const info = openAiCodexModels[id as keyof typeof openAiCodexModels] diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index aa73efefa0f..c36bfd3b5c9 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -455,6 +455,8 @@ "getDeepSeekApiKey": "Get DeepSeek API Key", "doubaoApiKey": "Doubao API Key", "getDoubaoApiKey": "Get Doubao API Key", + "qwenApiKey": "Qwen API Key", + "getQwenApiKey": "Get Qwen API Key", "moonshotApiKey": "Moonshot API Key", "getMoonshotApiKey": "Get Moonshot API Key", "moonshotBaseUrl": "Moonshot Entrypoint", From c94b6d1234f5b533f60b2c6fc581315dbc55dac3 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Thu, 5 Feb 2026 10:51:45 -0500 Subject: [PATCH 2/4] chore: add Qwen translations for all locales --- webview-ui/src/i18n/locales/ca/settings.json | 2 ++ webview-ui/src/i18n/locales/de/settings.json | 2 ++ webview-ui/src/i18n/locales/en/settings.json | 1 + webview-ui/src/i18n/locales/es/settings.json | 2 ++ webview-ui/src/i18n/locales/fr/settings.json | 2 ++ webview-ui/src/i18n/locales/hi/settings.json | 2 ++ webview-ui/src/i18n/locales/id/settings.json | 2 ++ webview-ui/src/i18n/locales/it/settings.json | 2 ++ webview-ui/src/i18n/locales/ja/settings.json | 2 ++ webview-ui/src/i18n/locales/ko/settings.json | 2 ++ webview-ui/src/i18n/locales/nl/settings.json | 2 ++ webview-ui/src/i18n/locales/pl/settings.json | 2 ++ webview-ui/src/i18n/locales/pt-BR/settings.json | 2 ++ webview-ui/src/i18n/locales/ru/settings.json | 2 ++ webview-ui/src/i18n/locales/tr/settings.json | 2 ++ webview-ui/src/i18n/locales/vi/settings.json | 2 ++ webview-ui/src/i18n/locales/zh-CN/settings.json | 2 ++ webview-ui/src/i18n/locales/zh-TW/settings.json | 2 ++ 18 files changed, 35 insertions(+) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index de245966128..8299687b323 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "Obtenir clau API de DeepSeek", "doubaoApiKey": "Clau API de Doubao", "getDoubaoApiKey": "Obtenir clau API de Doubao", + "qwenApiKey": "Clau API de Qwen", + "getQwenApiKey": "Obtenir clau API de Qwen", "moonshotApiKey": "Clau API de Moonshot", "getMoonshotApiKey": "Obtenir clau API de Moonshot", "moonshotBaseUrl": "Punt d'entrada de Moonshot", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 0dba82fe901..c7655d60636 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -318,6 +318,8 @@ "getVercelAiGatewayApiKey": "Vercel AI Gateway API-Schlüssel erhalten", "doubaoApiKey": "Doubao API-Schlüssel", "getDoubaoApiKey": "Doubao API-Schlüssel erhalten", + "qwenApiKey": "Qwen API-Schlüssel", + "getQwenApiKey": "Qwen API-Schlüssel erhalten", "apiKeyStorageNotice": "API-Schlüssel werden sicher im VSCode Secret Storage gespeichert", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index c36bfd3b5c9..f2cf8ef1603 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -457,6 +457,7 @@ "getDoubaoApiKey": "Get Doubao API Key", "qwenApiKey": "Qwen API Key", "getQwenApiKey": "Get Qwen API Key", + "getQwenApiKey": "Get Qwen API Key", "moonshotApiKey": "Moonshot API Key", "getMoonshotApiKey": "Get Moonshot API Key", "moonshotBaseUrl": "Moonshot Entrypoint", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index a8e7c250200..108737a4281 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "Obtener clave API de DeepSeek", "doubaoApiKey": "Clave API de Doubao", "getDoubaoApiKey": "Obtener clave API de Doubao", + "qwenApiKey": "Clave API de Qwen", + "getQwenApiKey": "Obtener clave API de Qwen", "moonshotApiKey": "Clave API de Moonshot", "getMoonshotApiKey": "Obtener clave API de Moonshot", "moonshotBaseUrl": "Punto de entrada de Moonshot", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 510dcf9dea1..4103629dd36 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "Obtenir la clé API DeepSeek", "doubaoApiKey": "Clé API Doubao", "getDoubaoApiKey": "Obtenir la clé API Doubao", + "qwenApiKey": "Clé API Qwen", + "getQwenApiKey": "Obtenir la clé API Qwen", "moonshotApiKey": "Clé API Moonshot", "getMoonshotApiKey": "Obtenir la clé API Moonshot", "moonshotBaseUrl": "Point d'entrée Moonshot", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 2abb829e7fa..7102bf597a7 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "DeepSeek API कुंजी प्राप्त करें", "doubaoApiKey": "डौबाओ API कुंजी", "getDoubaoApiKey": "डौबाओ API कुंजी प्राप्त करें", + "qwenApiKey": "क्वेन API कुंजी", + "getQwenApiKey": "क्वेन API कुंजी प्राप्त करें", "moonshotApiKey": "Moonshot API कुंजी", "getMoonshotApiKey": "Moonshot API कुंजी प्राप्त करें", "moonshotBaseUrl": "Moonshot प्रवेश बिंदु", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 2d23190db5b..d9b19088670 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "Dapatkan DeepSeek API Key", "doubaoApiKey": "Kunci API Doubao", "getDoubaoApiKey": "Dapatkan Kunci API Doubao", + "qwenApiKey": "Kunci API Qwen", + "getQwenApiKey": "Dapatkan Kunci API Qwen", "moonshotApiKey": "Kunci API Moonshot", "getMoonshotApiKey": "Dapatkan Kunci API Moonshot", "moonshotBaseUrl": "Titik Masuk Moonshot", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 3cf46a01277..b854df61119 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "Ottieni chiave API DeepSeek", "doubaoApiKey": "Chiave API Doubao", "getDoubaoApiKey": "Ottieni chiave API Doubao", + "qwenApiKey": "Chiave API Qwen", + "getQwenApiKey": "Ottieni chiave API Qwen", "moonshotApiKey": "Chiave API Moonshot", "getMoonshotApiKey": "Ottieni chiave API Moonshot", "moonshotBaseUrl": "Punto di ingresso Moonshot", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 0d60e45faad..5dff18ec071 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "DeepSeek APIキーを取得", "doubaoApiKey": "Doubao APIキー", "getDoubaoApiKey": "Doubao APIキーを取得", + "qwenApiKey": "Qwen APIキー", + "getQwenApiKey": "Qwen APIキーを取得", "moonshotApiKey": "Moonshot APIキー", "getMoonshotApiKey": "Moonshot APIキーを取得", "moonshotBaseUrl": "Moonshot エントリーポイント", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 79b848ec0bc..54508a12c14 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "DeepSeek API 키 받기", "doubaoApiKey": "Doubao API 키", "getDoubaoApiKey": "Doubao API 키 받기", + "qwenApiKey": "Qwen API 키", + "getQwenApiKey": "Qwen API 키 받기", "moonshotApiKey": "Moonshot API 키", "getMoonshotApiKey": "Moonshot API 키 받기", "moonshotBaseUrl": "Moonshot 엔트리포인트", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index f1c2c0c8184..644d2ed834c 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "DeepSeek API-sleutel ophalen", "doubaoApiKey": "Doubao API-sleutel", "getDoubaoApiKey": "Doubao API-sleutel ophalen", + "qwenApiKey": "Qwen API-sleutel", + "getQwenApiKey": "Qwen API-sleutel ophalen", "moonshotApiKey": "Moonshot API-sleutel", "getMoonshotApiKey": "Moonshot API-sleutel ophalen", "moonshotBaseUrl": "Moonshot-ingangspunt", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 25d05e3ca97..c951077b5b8 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "Uzyskaj klucz API DeepSeek", "doubaoApiKey": "Klucz API Doubao", "getDoubaoApiKey": "Uzyskaj klucz API Doubao", + "qwenApiKey": "Klucz API Qwen", + "getQwenApiKey": "Uzyskaj klucz API Qwen", "moonshotApiKey": "Klucz API Moonshot", "getMoonshotApiKey": "Uzyskaj klucz API Moonshot", "moonshotBaseUrl": "Punkt wejścia Moonshot", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 9fd8910935b..a15858635d2 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "Obter chave de API DeepSeek", "doubaoApiKey": "Chave de API Doubao", "getDoubaoApiKey": "Obter chave de API Doubao", + "qwenApiKey": "Chave de API Qwen", + "getQwenApiKey": "Obter chave de API Qwen", "moonshotApiKey": "Chave de API Moonshot", "getMoonshotApiKey": "Obter chave de API Moonshot", "moonshotBaseUrl": "Ponto de entrada Moonshot", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 4f6554e7503..0bf43bf4f4e 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "Получить DeepSeek API-ключ", "doubaoApiKey": "Doubao API-ключ", "getDoubaoApiKey": "Получить Doubao API-ключ", + "qwenApiKey": "Qwen API-ключ", + "getQwenApiKey": "Получить Qwen API-ключ", "moonshotApiKey": "Moonshot API-ключ", "getMoonshotApiKey": "Получить Moonshot API-ключ", "moonshotBaseUrl": "Точка входа Moonshot", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 83c393ef22e..7348429c008 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "DeepSeek API Anahtarı Al", "doubaoApiKey": "Doubao API Anahtarı", "getDoubaoApiKey": "Doubao API Anahtarı Al", + "qwenApiKey": "Qwen API Anahtarı", + "getQwenApiKey": "Qwen API Anahtarı Al", "moonshotApiKey": "Moonshot API Anahtarı", "getMoonshotApiKey": "Moonshot API Anahtarı Al", "moonshotBaseUrl": "Moonshot Giriş Noktası", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 8ec414d0984..67e271a79ba 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "Lấy khóa API DeepSeek", "doubaoApiKey": "Khóa API Doubao", "getDoubaoApiKey": "Lấy khóa API Doubao", + "qwenApiKey": "Khóa API Qwen", + "getQwenApiKey": "Lấy khóa API Qwen", "moonshotApiKey": "Khóa API Moonshot", "getMoonshotApiKey": "Lấy khóa API Moonshot", "moonshotBaseUrl": "Điểm vào Moonshot", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index a88d929ced4..7f15e625eef 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -392,6 +392,8 @@ "getDeepSeekApiKey": "获取 DeepSeek API 密钥", "doubaoApiKey": "豆包 API 密钥", "getDoubaoApiKey": "获取豆包 API 密钥", + "qwenApiKey": "Qwen API 密钥", + "getQwenApiKey": "获取 Qwen API 密钥", "moonshotApiKey": "Moonshot API 密钥", "getMoonshotApiKey": "获取 Moonshot API 密钥", "moonshotBaseUrl": "Moonshot 服务站点", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 0b64a3f01bd..9c716a546d7 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -402,6 +402,8 @@ "getDeepSeekApiKey": "取得 DeepSeek API 金鑰", "doubaoApiKey": "豆包 API 金鑰", "getDoubaoApiKey": "取得豆包 API 金鑰", + "qwenApiKey": "Qwen API 金鑰", + "getQwenApiKey": "取得 Qwen API 金鑰", "moonshotApiKey": "Moonshot API 金鑰", "getMoonshotApiKey": "取得 Moonshot API 金鑰", "moonshotBaseUrl": "Moonshot 服務端點", From 90ab1143ed9e19954be50159a431781987560420 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Fri, 6 Feb 2026 12:39:34 -0500 Subject: [PATCH 3/4] fix: remove unused import and duplicate JSON key - Remove unused QWEN_API_BASE_URL_CHINA import in qwen.ts - Remove duplicate getQwenApiKey key in en/settings.json --- src/api/providers/qwen.ts | 8 +------- webview-ui/src/i18n/locales/en/settings.json | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/api/providers/qwen.ts b/src/api/providers/qwen.ts index 416d1fa9cf0..82749572b90 100644 --- a/src/api/providers/qwen.ts +++ b/src/api/providers/qwen.ts @@ -2,13 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import { createQwen } from "qwen-ai-provider-v5" import { streamText, generateText, ToolSet } from "ai" -import { - qwenModels, - qwenDefaultModelId, - QWEN_API_BASE_URL, - QWEN_API_BASE_URL_CHINA, - type ModelInfo, -} from "@roo-code/types" +import { qwenModels, qwenDefaultModelId, QWEN_API_BASE_URL, type ModelInfo } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index f2cf8ef1603..c36bfd3b5c9 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -457,7 +457,6 @@ "getDoubaoApiKey": "Get Doubao API Key", "qwenApiKey": "Qwen API Key", "getQwenApiKey": "Get Qwen API Key", - "getQwenApiKey": "Get Qwen API Key", "moonshotApiKey": "Moonshot API Key", "getMoonshotApiKey": "Get Moonshot API Key", "moonshotBaseUrl": "Moonshot Entrypoint", From cf8d420231565c7abe855e373541cbbd026cd58b Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Fri, 6 Feb 2026 17:01:03 -0500 Subject: [PATCH 4/4] refactor: rename Qwen provider to Alibaba and switch to @ai-sdk/alibaba MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace qwen-ai-provider-v5 with @ai-sdk/alibaba package - Rename provider ID from 'qwen' to 'alibaba' across all files - Rename all identifiers: QwenHandler → AlibabaHandler, qwenApiKey → alibabaApiKey, etc. - Add thinking mode support via providerOptions.alibaba (enableThinking/thinkingBudget) - Enable prompt caching (supportsPromptCache: true) on all models - Add qwen3-max model with reasoning budget support - Update UI component, constants, translations (18 locales) - All 22 tests passing --- packages/types/src/provider-settings.ts | 18 ++-- .../src/providers/{qwen.ts => alibaba.ts} | 96 +++++++++++-------- packages/types/src/providers/index.ts | 8 +- pnpm-lock.yaml | 66 +++++++++---- src/api/index.ts | 6 +- .../{qwen.spec.ts => alibaba.spec.ts} | 65 ++++++------- src/api/providers/{qwen.ts => alibaba.ts} | 56 ++++++----- src/api/providers/index.ts | 2 +- src/package.json | 2 +- .../src/components/settings/ApiOptions.tsx | 10 +- .../src/components/settings/constants.ts | 6 +- .../providers/{Qwen.tsx => Alibaba.tsx} | 16 ++-- .../components/settings/providers/index.ts | 2 +- .../components/ui/hooks/useSelectedModel.ts | 6 +- 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 +- 32 files changed, 240 insertions(+), 191 deletions(-) rename packages/types/src/providers/{qwen.ts => alibaba.ts} (63%) rename src/api/providers/__tests__/{qwen.spec.ts => alibaba.spec.ts} (85%) rename src/api/providers/{qwen.ts => alibaba.ts} (73%) rename webview-ui/src/components/settings/providers/{Qwen.tsx => Alibaba.tsx} (74%) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index a4ecb2cb8a5..0d76381b0fa 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -19,7 +19,7 @@ import { openAiCodexModels, openAiNativeModels, qwenCodeModels, - qwenModels, + alibabaModels, sambaNovaModels, vertexModels, vscodeLlmModels, @@ -136,7 +136,7 @@ export const providerNames = [ "openai-codex", "openai-native", "qwen-code", - "qwen", + "alibaba", "roo", "sambanova", "vertex", @@ -405,9 +405,9 @@ const qwenCodeSchema = apiModelIdProviderModelSchema.extend({ qwenCodeOauthPath: z.string().optional(), }) -const qwenSchema = apiModelIdProviderModelSchema.extend({ - qwenApiKey: z.string().optional(), - qwenBaseUrl: z.string().optional(), +const alibabaSchema = apiModelIdProviderModelSchema.extend({ + alibabaApiKey: z.string().optional(), + alibabaBaseUrl: z.string().optional(), }) const rooSchema = apiModelIdProviderModelSchema.extend({ @@ -463,7 +463,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv featherlessSchema.merge(z.object({ apiProvider: z.literal("featherless") })), ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })), qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })), - qwenSchema.merge(z.object({ apiProvider: z.literal("qwen") })), + alibabaSchema.merge(z.object({ apiProvider: z.literal("alibaba") })), rooSchema.merge(z.object({ apiProvider: z.literal("roo") })), vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })), defaultSchema, @@ -505,7 +505,7 @@ export const providerSettingsSchema = z.object({ ...featherlessSchema.shape, ...ioIntelligenceSchema.shape, ...qwenCodeSchema.shape, - ...qwenSchema.shape, + ...alibabaSchema.shape, ...rooSchema.shape, ...vercelAiGatewaySchema.shape, ...codebaseIndexProviderSchema.shape, @@ -577,7 +577,7 @@ export const modelIdKeysByProvider: Record = { deepinfra: "deepInfraModelId", doubao: "apiModelId", "qwen-code": "apiModelId", - qwen: "apiModelId", + alibaba: "apiModelId", unbound: "unboundModelId", requesty: "requestyModelId", xai: "apiModelId", @@ -701,7 +701,7 @@ export const MODELS_BY_PROVIDER: Record< models: Object.keys(openAiNativeModels), }, "qwen-code": { id: "qwen-code", label: "Qwen Code", models: Object.keys(qwenCodeModels) }, - qwen: { id: "qwen", label: "Qwen", models: Object.keys(qwenModels) }, + alibaba: { id: "alibaba", label: "Alibaba", models: Object.keys(alibabaModels) }, roo: { id: "roo", label: "Roo Code Router", models: [] }, sambanova: { id: "sambanova", diff --git a/packages/types/src/providers/qwen.ts b/packages/types/src/providers/alibaba.ts similarity index 63% rename from packages/types/src/providers/qwen.ts rename to packages/types/src/providers/alibaba.ts index 7238b903bea..ec3854d6b3c 100644 --- a/packages/types/src/providers/qwen.ts +++ b/packages/types/src/providers/alibaba.ts @@ -1,6 +1,6 @@ import type { ModelInfo } from "../model.js" -export type QwenModelId = +export type AlibabaModelId = | "qwen-max" | "qwen-max-latest" | "qwen-plus" @@ -13,26 +13,29 @@ export type QwenModelId = | "qwen2.5-7b-instruct" | "qwen2.5-14b-instruct-1m" | "qwen3-235b-a22b" + | "qwen3-max" | "qwen-vl-max" | "qwen-vl-plus" -export const qwenDefaultModelId: QwenModelId = "qwen-plus" +export const alibabaDefaultModelId: AlibabaModelId = "qwen-plus" -export const qwenModels = { +export const alibabaModels = { "qwen-max": { maxTokens: 8_192, contextWindow: 32_768, supportsImages: false, - supportsPromptCache: false, - inputPrice: 2.0, // $2 per million tokens - outputPrice: 6.0, // $6 per million tokens + supportsPromptCache: true, + supportsReasoningBudget: true, + inputPrice: 2.0, + outputPrice: 6.0, description: "Qwen Max - Most capable model, best for complex reasoning and generation tasks", }, "qwen-max-latest": { maxTokens: 8_192, contextWindow: 32_768, supportsImages: false, - supportsPromptCache: false, + supportsPromptCache: true, + supportsReasoningBudget: true, inputPrice: 2.0, outputPrice: 6.0, description: "Qwen Max Latest - Latest version of Qwen Max with most recent improvements", @@ -41,16 +44,16 @@ export const qwenModels = { maxTokens: 8_192, contextWindow: 131_072, supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.4, // $0.40 per million tokens - outputPrice: 1.2, // $1.20 per million tokens + supportsPromptCache: true, + inputPrice: 0.4, + outputPrice: 1.2, description: "Qwen Plus - Balanced performance and cost for general tasks", }, "qwen-plus-latest": { maxTokens: 8_192, contextWindow: 131_072, supportsImages: false, - supportsPromptCache: false, + supportsPromptCache: true, inputPrice: 0.4, outputPrice: 1.2, description: "Qwen Plus Latest - Latest version of Qwen Plus", @@ -59,16 +62,16 @@ export const qwenModels = { maxTokens: 8_192, contextWindow: 131_072, supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.06, // $0.06 per million tokens - outputPrice: 0.24, // $0.24 per million tokens + supportsPromptCache: true, + inputPrice: 0.06, + outputPrice: 0.24, description: "Qwen Turbo - Fastest and most cost-effective for simple tasks", }, "qwen-turbo-latest": { maxTokens: 8_192, contextWindow: 131_072, supportsImages: false, - supportsPromptCache: false, + supportsPromptCache: true, inputPrice: 0.06, outputPrice: 0.24, description: "Qwen Turbo Latest - Latest version of Qwen Turbo", @@ -77,43 +80,43 @@ export const qwenModels = { maxTokens: 8_192, contextWindow: 131_072, supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.8, // $0.80 per million tokens - outputPrice: 2.4, // $2.40 per million tokens + supportsPromptCache: true, + inputPrice: 0.8, + outputPrice: 2.4, description: "Qwen 2.5 72B Instruct - Large open model with strong performance", }, "qwen2.5-32b-instruct": { maxTokens: 8_192, contextWindow: 131_072, supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.4, // $0.40 per million tokens - outputPrice: 1.2, // $1.20 per million tokens + supportsPromptCache: true, + inputPrice: 0.4, + outputPrice: 1.2, description: "Qwen 2.5 32B Instruct - Medium-sized model with good balance", }, "qwen2.5-14b-instruct": { maxTokens: 8_192, contextWindow: 131_072, supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.28, // $0.28 per million tokens - outputPrice: 0.84, // $0.84 per million tokens + supportsPromptCache: true, + inputPrice: 0.28, + outputPrice: 0.84, description: "Qwen 2.5 14B Instruct - Efficient model for various tasks", }, "qwen2.5-7b-instruct": { maxTokens: 8_192, contextWindow: 131_072, supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.14, // $0.14 per million tokens - outputPrice: 0.42, // $0.42 per million tokens + supportsPromptCache: true, + inputPrice: 0.14, + outputPrice: 0.42, description: "Qwen 2.5 7B Instruct - Lightweight model for fast inference", }, "qwen2.5-14b-instruct-1m": { maxTokens: 8_192, contextWindow: 1_000_000, supportsImages: false, - supportsPromptCache: false, + supportsPromptCache: true, inputPrice: 0.28, outputPrice: 0.84, description: "Qwen 2.5 14B Instruct 1M - Extended 1M context window variant", @@ -122,35 +125,46 @@ export const qwenModels = { maxTokens: 8_192, contextWindow: 131_072, supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.8, // $0.80 per million tokens - outputPrice: 2.4, // $2.40 per million tokens + supportsPromptCache: true, + supportsReasoningBudget: true, + inputPrice: 0.8, + outputPrice: 2.4, description: "Qwen 3 235B A22B - Largest Qwen 3 model with 235B parameters in MoE architecture", }, + "qwen3-max": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: true, + supportsReasoningBudget: true, + inputPrice: 1.6, + outputPrice: 4.8, + description: "Qwen 3 Max - Most capable Qwen 3 model with thinking mode support", + }, "qwen-vl-max": { maxTokens: 8_192, contextWindow: 32_768, supportsImages: true, - supportsPromptCache: false, - inputPrice: 3.0, // $3 per million tokens - outputPrice: 9.0, // $9 per million tokens + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 9.0, description: "Qwen VL Max - Best vision model, supports images and video understanding", }, "qwen-vl-plus": { maxTokens: 8_192, contextWindow: 32_768, supportsImages: true, - supportsPromptCache: false, - inputPrice: 1.2, // $1.20 per million tokens - outputPrice: 3.6, // $3.60 per million tokens + supportsPromptCache: true, + inputPrice: 1.2, + outputPrice: 3.6, description: "Qwen VL Plus - Balanced vision model for image understanding tasks", }, -} as const satisfies Record +} as const satisfies Record -export const qwenDefaultModelInfo: ModelInfo = qwenModels[qwenDefaultModelId] +export const alibabaDefaultModelInfo: ModelInfo = alibabaModels[alibabaDefaultModelId] // International endpoint (default) -export const QWEN_API_BASE_URL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" +export const ALIBABA_API_BASE_URL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" // China domestic endpoint -export const QWEN_API_BASE_URL_CHINA = "https://dashscope.aliyuncs.com/compatible-mode/v1" +export const ALIBABA_API_BASE_URL_CHINA = "https://dashscope.aliyuncs.com/compatible-mode/v1" diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 716505e59d8..34a6615cd98 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -21,7 +21,7 @@ export * from "./openai-codex.js" export * from "./openai-codex-rate-limits.js" export * from "./openrouter.js" export * from "./qwen-code.js" -export * from "./qwen.js" +export * from "./alibaba.js" export * from "./requesty.js" export * from "./roo.js" export * from "./sambanova.js" @@ -52,7 +52,7 @@ import { moonshotDefaultModelId } from "./moonshot.js" import { openAiCodexDefaultModelId } from "./openai-codex.js" import { openRouterDefaultModelId } from "./openrouter.js" import { qwenCodeDefaultModelId } from "./qwen-code.js" -import { qwenDefaultModelId } from "./qwen.js" +import { alibabaDefaultModelId } from "./alibaba.js" import { requestyDefaultModelId } from "./requesty.js" import { rooDefaultModelId } from "./roo.js" import { sambaNovaDefaultModelId } from "./sambanova.js" @@ -142,8 +142,8 @@ export function getProviderDefaultModelId( return rooDefaultModelId case "qwen-code": return qwenCodeDefaultModelId - case "qwen": - return qwenDefaultModelId + case "alibaba": + return alibabaDefaultModelId case "vercel-ai-gateway": return vercelAiGatewayDefaultModelId case "anthropic": diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ebff4a0b1a..eb24e18539e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -746,6 +746,9 @@ importers: src: dependencies: + '@ai-sdk/alibaba': + specifier: ^1.0.0 + version: 1.0.1(zod@3.25.76) '@ai-sdk/cerebras': specifier: ^1.0.0 version: 1.0.35(zod@3.25.76) @@ -935,9 +938,6 @@ importers: puppeteer-core: specifier: ^23.4.0 version: 23.11.1 - qwen-ai-provider-v5: - specifier: ^2.1.0 - version: 2.1.0(zod@3.25.76) reconnecting-eventsource: specifier: ^1.6.4 version: 1.6.4 @@ -1414,6 +1414,12 @@ packages: '@adobe/css-tools@4.4.2': resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} + '@ai-sdk/alibaba@1.0.1': + resolution: {integrity: sha512-1IIlq246e9vShWaA2C++VB/Rr0x7mib6E4Xt1+SPygZpm+y5PhswfcuI/p0zdM/LDLdHUNRCptnJvVvPICJdHA==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + '@ai-sdk/cerebras@1.0.35': resolution: {integrity: sha512-JrNdMYptrOUjNthibgBeAcBjZ/H+fXb49sSrWhOx5Aq8eUcrYvwQ2DtSAi8VraHssZu78NAnBMrgFWSUOTXFxw==} engines: {node: '>=18'} @@ -1474,6 +1480,12 @@ packages: peerDependencies: zod: 3.25.76 + '@ai-sdk/openai-compatible@2.0.28': + resolution: {integrity: sha512-WzDnU0B13FMSSupDtm2lksFZvWGXnOfhG5S0HoPI0pkX5uVkr6N1UTATMyVaxLCG0MRkMhXCjkg4NXgEbb330Q==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + '@ai-sdk/provider-utils@3.0.20': resolution: {integrity: sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ==} engines: {node: '>=18'} @@ -1504,6 +1516,12 @@ packages: peerDependencies: zod: 3.25.76 + '@ai-sdk/provider-utils@4.0.14': + resolution: {integrity: sha512-7bzKd9lgiDeXM7O4U4nQ8iTxguAOkg8LZGD9AfDVZYjO5cKYRwBPwVjboFcVrxncRHu0tYxZtXZtiLKpG4pEng==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + '@ai-sdk/provider@2.0.0': resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} engines: {node: '>=18'} @@ -1524,6 +1542,10 @@ packages: resolution: {integrity: sha512-VkPLrutM6VdA924/mG8OS+5frbVTcu6e046D2bgDo00tehBANR1QBJ/mPcZ9tXMFOsVcm6SQArOregxePzTFPw==} engines: {node: '>=18'} + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} + engines: {node: '>=18'} + '@ai-sdk/xai@3.0.46': resolution: {integrity: sha512-26qM/jYcFhF5krTM7bQT1CiZcdz22EQmA+r5me1hKYFM/yM20sSUMHnAcUzvzuuG9oQVKF0tziU2IcC0HX5huQ==} engines: {node: '>=18'} @@ -9158,12 +9180,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - qwen-ai-provider-v5@2.1.0: - resolution: {integrity: sha512-I+Iv45ymrez1wieZFu0n/lc/lSkbAQMlujWBCfUWBUOf6DizYfvPKaydsojXM7CU8TcqJbYJuN3ofnaxFIwBZA==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: 3.25.76 - randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -11109,6 +11125,13 @@ snapshots: '@adobe/css-tools@4.4.2': {} + '@ai-sdk/alibaba@1.0.1(zod@3.25.76)': + dependencies: + '@ai-sdk/openai-compatible': 2.0.28(zod@3.25.76) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/cerebras@1.0.35(zod@3.25.76)': dependencies: '@ai-sdk/openai-compatible': 1.0.31(zod@3.25.76) @@ -11172,6 +11195,12 @@ snapshots: '@ai-sdk/provider-utils': 4.0.13(zod@3.25.76) zod: 3.25.76 + '@ai-sdk/openai-compatible@2.0.28(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/provider-utils@3.0.20(zod@3.25.76)': dependencies: '@ai-sdk/provider': 2.0.1 @@ -11208,6 +11237,13 @@ snapshots: eventsource-parser: 3.0.6 zod: 3.25.76 + '@ai-sdk/provider-utils@4.0.14(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + '@ai-sdk/provider@2.0.0': dependencies: json-schema: 0.4.0 @@ -11228,6 +11264,10 @@ snapshots: dependencies: json-schema: 0.4.0 + '@ai-sdk/provider@3.0.8': + dependencies: + json-schema: 0.4.0 + '@ai-sdk/xai@3.0.46(zod@3.25.76)': dependencies: '@ai-sdk/openai-compatible': 2.0.26(zod@3.25.76) @@ -15211,7 +15251,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -20176,12 +20216,6 @@ snapshots: queue-microtask@1.2.3: {} - qwen-ai-provider-v5@2.1.0(zod@3.25.76): - dependencies: - '@ai-sdk/provider': 3.0.7 - '@ai-sdk/provider-utils': 4.0.13(zod@3.25.76) - zod: 3.25.76 - randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 diff --git a/src/api/index.ts b/src/api/index.ts index 8f471a0466e..22ea3c025b9 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -30,7 +30,7 @@ import { ChutesHandler, LiteLLMHandler, QwenCodeHandler, - QwenHandler, + AlibabaHandler, SambaNovaHandler, IOIntelligenceHandler, DoubaoHandler, @@ -152,8 +152,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new DoubaoHandler(options) case "qwen-code": return new QwenCodeHandler(options) - case "qwen": - return new QwenHandler(options) + case "alibaba": + return new AlibabaHandler(options) case "moonshot": return new MoonshotHandler(options) case "vscode-lm": diff --git a/src/api/providers/__tests__/qwen.spec.ts b/src/api/providers/__tests__/alibaba.spec.ts similarity index 85% rename from src/api/providers/__tests__/qwen.spec.ts rename to src/api/providers/__tests__/alibaba.spec.ts index 543b4396aa0..d37c5e306f0 100644 --- a/src/api/providers/__tests__/qwen.spec.ts +++ b/src/api/providers/__tests__/alibaba.spec.ts @@ -13,66 +13,65 @@ vi.mock("ai", async (importOriginal) => { } }) -vi.mock("qwen-ai-provider-v5", () => ({ - createQwen: vi.fn(() => { - // Return a function that returns a mock language model +vi.mock("@ai-sdk/alibaba", () => ({ + createAlibaba: vi.fn(() => { return vi.fn(() => ({ modelId: "qwen-plus", - provider: "qwen", + provider: "alibaba", })) }), })) import type { Anthropic } from "@anthropic-ai/sdk" -import { qwenDefaultModelId } from "@roo-code/types" +import { alibabaDefaultModelId } from "@roo-code/types" import type { ApiHandlerOptions } from "../../../shared/api" -import { QwenHandler } from "../qwen" +import { AlibabaHandler } from "../alibaba" -describe("QwenHandler", () => { - let handler: QwenHandler +describe("AlibabaHandler", () => { + let handler: AlibabaHandler let mockOptions: ApiHandlerOptions beforeEach(() => { mockOptions = { - qwenApiKey: "test-api-key", + alibabaApiKey: "test-api-key", apiModelId: "qwen-plus", } - handler = new QwenHandler(mockOptions) + handler = new AlibabaHandler(mockOptions) vi.clearAllMocks() }) describe("constructor", () => { it("should initialize with provided options", () => { - expect(handler).toBeInstanceOf(QwenHandler) + expect(handler).toBeInstanceOf(AlibabaHandler) expect(handler.getModel().id).toBe(mockOptions.apiModelId) }) it("should use default model ID if not provided", () => { - const handlerWithoutModel = new QwenHandler({ + const handlerWithoutModel = new AlibabaHandler({ ...mockOptions, apiModelId: undefined, }) - expect(handlerWithoutModel.getModel().id).toBe(qwenDefaultModelId) + expect(handlerWithoutModel.getModel().id).toBe(alibabaDefaultModelId) }) it("should use default base URL if not provided", () => { - const handlerWithoutBaseUrl = new QwenHandler({ + const handlerWithoutBaseUrl = new AlibabaHandler({ ...mockOptions, - qwenBaseUrl: undefined, + alibabaBaseUrl: undefined, }) - expect(handlerWithoutBaseUrl).toBeInstanceOf(QwenHandler) + expect(handlerWithoutBaseUrl).toBeInstanceOf(AlibabaHandler) }) it("should use custom base URL if provided", () => { const customBaseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1" - const handlerWithCustomUrl = new QwenHandler({ + const handlerWithCustomUrl = new AlibabaHandler({ ...mockOptions, - qwenBaseUrl: customBaseUrl, + alibabaBaseUrl: customBaseUrl, }) - expect(handlerWithCustomUrl).toBeInstanceOf(QwenHandler) + expect(handlerWithCustomUrl).toBeInstanceOf(AlibabaHandler) }) }) @@ -81,14 +80,14 @@ describe("QwenHandler", () => { const model = handler.getModel() expect(model.id).toBe(mockOptions.apiModelId) expect(model.info).toBeDefined() - expect(model.info.maxTokens).toBe(8_192) // qwen-plus has 8K max output + expect(model.info.maxTokens).toBe(8_192) expect(model.info.contextWindow).toBe(131_072) expect(model.info.supportsImages).toBe(false) - expect(model.info.supportsPromptCache).toBe(false) + expect(model.info.supportsPromptCache).toBe(true) }) it("should return correct model info for qwen-max", () => { - const handlerWithMax = new QwenHandler({ + const handlerWithMax = new AlibabaHandler({ ...mockOptions, apiModelId: "qwen-max", }) @@ -101,7 +100,7 @@ describe("QwenHandler", () => { }) it("should return correct model info for qwen-vl-max (vision model)", () => { - const handlerWithVision = new QwenHandler({ + const handlerWithVision = new AlibabaHandler({ ...mockOptions, apiModelId: "qwen-vl-max", }) @@ -112,24 +111,23 @@ describe("QwenHandler", () => { }) it("should return provided model ID with default model info if model does not exist", () => { - const handlerWithInvalidModel = new QwenHandler({ + const handlerWithInvalidModel = new AlibabaHandler({ ...mockOptions, apiModelId: "invalid-model", }) const model = handlerWithInvalidModel.getModel() - expect(model.id).toBe("invalid-model") // Returns provided ID + expect(model.id).toBe("invalid-model") expect(model.info).toBeDefined() - // With the current implementation, it's the same object reference when using default model info expect(model.info).toBe(handler.getModel().info) }) it("should return default model if no model ID is provided", () => { - const handlerWithoutModel = new QwenHandler({ + const handlerWithoutModel = new AlibabaHandler({ ...mockOptions, apiModelId: undefined, }) const model = handlerWithoutModel.getModel() - expect(model.id).toBe(qwenDefaultModelId) + expect(model.id).toBe(alibabaDefaultModelId) expect(model.info).toBeDefined() }) @@ -155,12 +153,10 @@ describe("QwenHandler", () => { ] it("should handle streaming responses", async () => { - // Mock the fullStream async generator async function* mockFullStream() { yield { type: "text-delta", text: "Test response" } } - // Mock usage promise const mockUsage = Promise.resolve({ inputTokens: 10, outputTokens: 5, @@ -195,7 +191,6 @@ describe("QwenHandler", () => { }) const stream = handler.createMessage(systemPrompt, messages) - // Consume the stream for await (const _ of stream) { // Just consume } @@ -288,9 +283,7 @@ describe("QwenHandler", () => { chunks.push(chunk) } - // Should have text chunks expect(chunks.some((c: any) => c?.type === "text")).toBe(true) - // Should have tool call start, delta, and end chunks expect(chunks.some((c: any) => c?.type === "tool_call_start")).toBe(true) expect(chunks.some((c: any) => c?.type === "tool_call_delta")).toBe(true) expect(chunks.some((c: any) => c?.type === "tool_call_end")).toBe(true) @@ -374,7 +367,7 @@ describe("QwenHandler", () => { describe("model variants", () => { it("should handle qwen-turbo model", () => { - const turboHandler = new QwenHandler({ + const turboHandler = new AlibabaHandler({ ...mockOptions, apiModelId: "qwen-turbo", }) @@ -385,7 +378,7 @@ describe("QwenHandler", () => { }) it("should handle qwen2.5-72b-instruct model", () => { - const modelHandler = new QwenHandler({ + const modelHandler = new AlibabaHandler({ ...mockOptions, apiModelId: "qwen2.5-72b-instruct", }) @@ -395,7 +388,7 @@ describe("QwenHandler", () => { }) it("should handle qwen2.5-14b-instruct-1m model with 1M context", () => { - const modelHandler = new QwenHandler({ + const modelHandler = new AlibabaHandler({ ...mockOptions, apiModelId: "qwen2.5-14b-instruct-1m", }) diff --git a/src/api/providers/qwen.ts b/src/api/providers/alibaba.ts similarity index 73% rename from src/api/providers/qwen.ts rename to src/api/providers/alibaba.ts index 82749572b90..7e058af892a 100644 --- a/src/api/providers/qwen.ts +++ b/src/api/providers/alibaba.ts @@ -1,8 +1,8 @@ import { Anthropic } from "@anthropic-ai/sdk" -import { createQwen } from "qwen-ai-provider-v5" +import { createAlibaba } from "@ai-sdk/alibaba" import { streamText, generateText, ToolSet } from "ai" -import { qwenModels, qwenDefaultModelId, QWEN_API_BASE_URL, type ModelInfo } from "@roo-code/types" +import { alibabaModels, alibabaDefaultModelId, ALIBABA_API_BASE_URL, type ModelInfo } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" @@ -20,34 +20,32 @@ import { DEFAULT_HEADERS } from "./constants" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" -export const QWEN_DEFAULT_TEMPERATURE = 0 +export const ALIBABA_DEFAULT_TEMPERATURE = 0 /** - * Qwen provider using the qwen-ai-provider-v5 AI SDK package. + * Alibaba provider using the @ai-sdk/alibaba AI SDK package. * Provides access to Alibaba Cloud's Qwen models via DashScope platform. */ -export class QwenHandler extends BaseProvider implements SingleCompletionHandler { +export class AlibabaHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions - protected provider: ReturnType + protected provider: ReturnType constructor(options: ApiHandlerOptions) { super() this.options = options - // Determine base URL - use China endpoint if specified - const baseURL = options.qwenBaseUrl ?? QWEN_API_BASE_URL + const baseURL = options.alibabaBaseUrl ?? ALIBABA_API_BASE_URL - // Create the Qwen provider using AI SDK - this.provider = createQwen({ + this.provider = createAlibaba({ baseURL, - apiKey: options.qwenApiKey ?? "not-provided", + apiKey: options.alibabaApiKey ?? "not-provided", headers: DEFAULT_HEADERS, }) } override getModel(): { id: string; info: ModelInfo; maxTokens?: number; temperature?: number } { - const id = this.options.apiModelId ?? qwenDefaultModelId - const info = qwenModels[id as keyof typeof qwenModels] || qwenModels[qwenDefaultModelId] + const id = this.options.apiModelId ?? alibabaDefaultModelId + const info = alibabaModels[id as keyof typeof alibabaModels] || alibabaModels[alibabaDefaultModelId] const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options }) return { id, info, ...params } } @@ -62,7 +60,6 @@ export class QwenHandler extends BaseProvider implements SingleCompletionHandler /** * Process usage metrics from the AI SDK response. - * Qwen provides standard usage metrics. */ protected processUsageMetrics(usage: { inputTokens?: number @@ -89,6 +86,22 @@ export class QwenHandler extends BaseProvider implements SingleCompletionHandler return this.options.modelMaxTokens || info.maxTokens || undefined } + /** + * Build provider options for thinking mode support. + */ + protected getProviderOptions() { + const { info } = this.getModel() + if (info.supportsReasoningBudget && this.options.modelMaxThinkingTokens) { + return { + alibaba: { + enableThinking: true, + thinkingBudget: this.options.modelMaxThinkingTokens, + }, + } as const + } + return undefined + } + /** * Create a message stream using the AI SDK. */ @@ -100,43 +113,37 @@ export class QwenHandler extends BaseProvider implements SingleCompletionHandler const { temperature } = this.getModel() const languageModel = this.getLanguageModel() - // Convert messages to AI SDK format const aiSdkMessages = convertToAiSdkMessages(messages) - // Convert tools to OpenAI format first, then to AI SDK format const openAiTools = this.convertToolsForOpenAI(metadata?.tools) const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined - // Build the request options const requestOptions: Parameters[0] = { model: languageModel, system: systemPrompt, messages: aiSdkMessages, - temperature: this.options.modelTemperature ?? temperature ?? QWEN_DEFAULT_TEMPERATURE, + temperature: this.options.modelTemperature ?? temperature ?? ALIBABA_DEFAULT_TEMPERATURE, maxOutputTokens: this.getMaxOutputTokens(), tools: aiSdkTools, toolChoice: mapToolChoice(metadata?.tool_choice), + providerOptions: this.getProviderOptions(), } - // Use streamText for streaming responses const result = streamText(requestOptions) try { - // Process the full stream to get all events for await (const part of result.fullStream) { for (const chunk of processAiSdkStreamPart(part)) { yield chunk } } - // Yield usage metrics at the end const usage = await result.usage if (usage) { yield this.processUsageMetrics(usage) } } catch (error) { - // Handle AI SDK errors (AI_RetryError, AI_APICallError, etc.) - throw handleAiSdkError(error, "Qwen") + throw handleAiSdkError(error, "Alibaba") } } @@ -151,7 +158,8 @@ export class QwenHandler extends BaseProvider implements SingleCompletionHandler model: languageModel, prompt, maxOutputTokens: this.getMaxOutputTokens(), - temperature: this.options.modelTemperature ?? temperature ?? QWEN_DEFAULT_TEMPERATURE, + temperature: this.options.modelTemperature ?? temperature ?? ALIBABA_DEFAULT_TEMPERATURE, + providerOptions: this.getProviderOptions(), }) return text diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 019f14818f3..2a35c512a04 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -21,7 +21,7 @@ export { OpenAICompatibleHandler } from "./openai-compatible" export type { OpenAICompatibleConfig } from "./openai-compatible" export { OpenRouterHandler } from "./openrouter" export { QwenCodeHandler } from "./qwen-code" -export { QwenHandler } from "./qwen" +export { AlibabaHandler } from "./alibaba" export { RequestyHandler } from "./requesty" export { SambaNovaHandler } from "./sambanova" export { UnboundHandler } from "./unbound" diff --git a/src/package.json b/src/package.json index 55d5fabb3ec..ffbd13a6cdf 100644 --- a/src/package.json +++ b/src/package.json @@ -513,7 +513,7 @@ "ps-tree": "^1.2.0", "puppeteer-chromium-resolver": "^24.0.0", "puppeteer-core": "^23.4.0", - "qwen-ai-provider-v5": "^2.1.0", + "@ai-sdk/alibaba": "^1.0.0", "reconnecting-eventsource": "^1.6.4", "safe-stable-stringify": "^2.5.0", "sambanova-ai-provider": "^1.2.2", diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 4e51972099d..de862f6511c 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -17,7 +17,7 @@ import { anthropicDefaultModelId, doubaoDefaultModelId, qwenCodeDefaultModelId, - qwenDefaultModelId, + alibabaDefaultModelId, geminiDefaultModelId, deepSeekDefaultModelId, moonshotDefaultModelId, @@ -94,7 +94,7 @@ import { OpenAICodex, OpenRouter, QwenCode, - Qwen, + Alibaba, Requesty, Roo, SambaNova, @@ -347,7 +347,7 @@ const ApiOptions = ({ cerebras: { field: "apiModelId", default: cerebrasDefaultModelId }, "openai-codex": { field: "apiModelId", default: openAiCodexDefaultModelId }, "qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId }, - qwen: { field: "apiModelId", default: qwenDefaultModelId }, + alibaba: { field: "apiModelId", default: alibabaDefaultModelId }, "openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId }, gemini: { field: "apiModelId", default: geminiDefaultModelId }, deepseek: { field: "apiModelId", default: deepSeekDefaultModelId }, @@ -657,8 +657,8 @@ const ApiOptions = ({ /> )} - {selectedProvider === "qwen" && ( - void simplifySettings?: boolean } -export const Qwen = ({ apiConfiguration, setApiConfigurationField }: QwenProps) => { +export const Alibaba = ({ apiConfiguration, setApiConfigurationField }: AlibabaProps) => { const { t } = useAppTranslation() const handleInputChange = useCallback( @@ -31,19 +31,19 @@ export const Qwen = ({ apiConfiguration, setApiConfigurationField }: QwenProps) return ( <> - +
{t("settings:providers.apiKeyStorageNotice")}
- {!apiConfiguration?.qwenApiKey && ( - - {t("settings:providers.getQwenApiKey")} + {!apiConfiguration?.alibabaApiKey && ( + + {t("settings:providers.getAlibabaApiKey")} )} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index c7adb770483..af2be01aeb5 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -17,7 +17,7 @@ export { OpenAICodex } from "./OpenAICodex" export { OpenAICompatible } from "./OpenAICompatible" export { OpenRouter } from "./OpenRouter" export { QwenCode } from "./QwenCode" -export { Qwen } from "./Qwen" +export { Alibaba } from "./Alibaba" export { Roo } from "./Roo" export { Requesty } from "./Requesty" export { SambaNova } from "./SambaNova" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index c2bfdc18b71..6e7d2d3e22d 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -29,7 +29,7 @@ import { ioIntelligenceModels, basetenModels, qwenCodeModels, - qwenModels, + alibabaModels, litellmDefaultModelInfo, lMStudioDefaultModelInfo, BEDROCK_1M_CONTEXT_MODEL_IDS, @@ -355,9 +355,9 @@ function getSelectedModel({ const info = qwenCodeModels[id as keyof typeof qwenCodeModels] return { id, info } } - case "qwen": { + case "alibaba": { const id = apiConfiguration.apiModelId ?? defaultModelId - const info = qwenModels[id as keyof typeof qwenModels] + const info = alibabaModels[id as keyof typeof alibabaModels] return { id, info } } case "openai-codex": { diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 8299687b323..dca6a0c3cc5 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "Obtenir clau API de DeepSeek", "doubaoApiKey": "Clau API de Doubao", "getDoubaoApiKey": "Obtenir clau API de Doubao", - "qwenApiKey": "Clau API de Qwen", - "getQwenApiKey": "Obtenir clau API de Qwen", + "alibabaApiKey": "Clau API de Qwen", + "getAlibabaApiKey": "Obtenir clau API de Qwen", "moonshotApiKey": "Clau API de Moonshot", "getMoonshotApiKey": "Obtenir clau API de Moonshot", "moonshotBaseUrl": "Punt d'entrada de Moonshot", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index c7655d60636..5d817ff1c04 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -318,8 +318,8 @@ "getVercelAiGatewayApiKey": "Vercel AI Gateway API-Schlüssel erhalten", "doubaoApiKey": "Doubao API-Schlüssel", "getDoubaoApiKey": "Doubao API-Schlüssel erhalten", - "qwenApiKey": "Qwen API-Schlüssel", - "getQwenApiKey": "Qwen API-Schlüssel erhalten", + "alibabaApiKey": "Alibaba API-Schlüssel", + "getAlibabaApiKey": "Alibaba API-Schlüssel erhalten", "apiKeyStorageNotice": "API-Schlüssel werden sicher im VSCode Secret Storage gespeichert", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index c36bfd3b5c9..c1b82599bcc 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -455,8 +455,8 @@ "getDeepSeekApiKey": "Get DeepSeek API Key", "doubaoApiKey": "Doubao API Key", "getDoubaoApiKey": "Get Doubao API Key", - "qwenApiKey": "Qwen API Key", - "getQwenApiKey": "Get Qwen API Key", + "alibabaApiKey": "Alibaba API Key", + "getAlibabaApiKey": "Get Alibaba API Key", "moonshotApiKey": "Moonshot API Key", "getMoonshotApiKey": "Get Moonshot API Key", "moonshotBaseUrl": "Moonshot Entrypoint", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 108737a4281..28da639e7a9 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "Obtener clave API de DeepSeek", "doubaoApiKey": "Clave API de Doubao", "getDoubaoApiKey": "Obtener clave API de Doubao", - "qwenApiKey": "Clave API de Qwen", - "getQwenApiKey": "Obtener clave API de Qwen", + "alibabaApiKey": "Clave API de Qwen", + "getAlibabaApiKey": "Obtener clave API de Qwen", "moonshotApiKey": "Clave API de Moonshot", "getMoonshotApiKey": "Obtener clave API de Moonshot", "moonshotBaseUrl": "Punto de entrada de Moonshot", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 4103629dd36..5d670db327b 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "Obtenir la clé API DeepSeek", "doubaoApiKey": "Clé API Doubao", "getDoubaoApiKey": "Obtenir la clé API Doubao", - "qwenApiKey": "Clé API Qwen", - "getQwenApiKey": "Obtenir la clé API Qwen", + "alibabaApiKey": "Clé API Qwen", + "getAlibabaApiKey": "Obtenir la clé API Qwen", "moonshotApiKey": "Clé API Moonshot", "getMoonshotApiKey": "Obtenir la clé API Moonshot", "moonshotBaseUrl": "Point d'entrée Moonshot", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 7102bf597a7..2e330df1c78 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "DeepSeek API कुंजी प्राप्त करें", "doubaoApiKey": "डौबाओ API कुंजी", "getDoubaoApiKey": "डौबाओ API कुंजी प्राप्त करें", - "qwenApiKey": "क्वेन API कुंजी", - "getQwenApiKey": "क्वेन API कुंजी प्राप्त करें", + "alibabaApiKey": "क्वेन API कुंजी", + "getAlibabaApiKey": "क्वेन API कुंजी प्राप्त करें", "moonshotApiKey": "Moonshot API कुंजी", "getMoonshotApiKey": "Moonshot API कुंजी प्राप्त करें", "moonshotBaseUrl": "Moonshot प्रवेश बिंदु", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index d9b19088670..a2c3098458d 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "Dapatkan DeepSeek API Key", "doubaoApiKey": "Kunci API Doubao", "getDoubaoApiKey": "Dapatkan Kunci API Doubao", - "qwenApiKey": "Kunci API Qwen", - "getQwenApiKey": "Dapatkan Kunci API Qwen", + "alibabaApiKey": "Kunci API Qwen", + "getAlibabaApiKey": "Dapatkan Kunci API Qwen", "moonshotApiKey": "Kunci API Moonshot", "getMoonshotApiKey": "Dapatkan Kunci API Moonshot", "moonshotBaseUrl": "Titik Masuk Moonshot", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index b854df61119..4a419e38296 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "Ottieni chiave API DeepSeek", "doubaoApiKey": "Chiave API Doubao", "getDoubaoApiKey": "Ottieni chiave API Doubao", - "qwenApiKey": "Chiave API Qwen", - "getQwenApiKey": "Ottieni chiave API Qwen", + "alibabaApiKey": "Chiave API Qwen", + "getAlibabaApiKey": "Ottieni chiave API Qwen", "moonshotApiKey": "Chiave API Moonshot", "getMoonshotApiKey": "Ottieni chiave API Moonshot", "moonshotBaseUrl": "Punto di ingresso Moonshot", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 5dff18ec071..d0b6ab805ac 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "DeepSeek APIキーを取得", "doubaoApiKey": "Doubao APIキー", "getDoubaoApiKey": "Doubao APIキーを取得", - "qwenApiKey": "Qwen APIキー", - "getQwenApiKey": "Qwen APIキーを取得", + "alibabaApiKey": "Alibaba APIキー", + "getAlibabaApiKey": "Alibaba APIキーを取得", "moonshotApiKey": "Moonshot APIキー", "getMoonshotApiKey": "Moonshot APIキーを取得", "moonshotBaseUrl": "Moonshot エントリーポイント", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 54508a12c14..6f8db15acec 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "DeepSeek API 키 받기", "doubaoApiKey": "Doubao API 키", "getDoubaoApiKey": "Doubao API 키 받기", - "qwenApiKey": "Qwen API 키", - "getQwenApiKey": "Qwen API 키 받기", + "alibabaApiKey": "Alibaba API 키", + "getAlibabaApiKey": "Alibaba API 키 받기", "moonshotApiKey": "Moonshot API 키", "getMoonshotApiKey": "Moonshot API 키 받기", "moonshotBaseUrl": "Moonshot 엔트리포인트", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 644d2ed834c..f61f8b48d69 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "DeepSeek API-sleutel ophalen", "doubaoApiKey": "Doubao API-sleutel", "getDoubaoApiKey": "Doubao API-sleutel ophalen", - "qwenApiKey": "Qwen API-sleutel", - "getQwenApiKey": "Qwen API-sleutel ophalen", + "alibabaApiKey": "Alibaba API-sleutel", + "getAlibabaApiKey": "Alibaba API-sleutel ophalen", "moonshotApiKey": "Moonshot API-sleutel", "getMoonshotApiKey": "Moonshot API-sleutel ophalen", "moonshotBaseUrl": "Moonshot-ingangspunt", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index c951077b5b8..07656b52aee 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "Uzyskaj klucz API DeepSeek", "doubaoApiKey": "Klucz API Doubao", "getDoubaoApiKey": "Uzyskaj klucz API Doubao", - "qwenApiKey": "Klucz API Qwen", - "getQwenApiKey": "Uzyskaj klucz API Qwen", + "alibabaApiKey": "Klucz API Qwen", + "getAlibabaApiKey": "Uzyskaj klucz API Qwen", "moonshotApiKey": "Klucz API Moonshot", "getMoonshotApiKey": "Uzyskaj klucz API Moonshot", "moonshotBaseUrl": "Punkt wejścia Moonshot", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index a15858635d2..b66a5d9137c 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "Obter chave de API DeepSeek", "doubaoApiKey": "Chave de API Doubao", "getDoubaoApiKey": "Obter chave de API Doubao", - "qwenApiKey": "Chave de API Qwen", - "getQwenApiKey": "Obter chave de API Qwen", + "alibabaApiKey": "Chave de API Qwen", + "getAlibabaApiKey": "Obter chave de API Qwen", "moonshotApiKey": "Chave de API Moonshot", "getMoonshotApiKey": "Obter chave de API Moonshot", "moonshotBaseUrl": "Ponto de entrada Moonshot", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 0bf43bf4f4e..b90c2104589 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "Получить DeepSeek API-ключ", "doubaoApiKey": "Doubao API-ключ", "getDoubaoApiKey": "Получить Doubao API-ключ", - "qwenApiKey": "Qwen API-ключ", - "getQwenApiKey": "Получить Qwen API-ключ", + "alibabaApiKey": "Alibaba API-ключ", + "getAlibabaApiKey": "Получить Alibaba API-ключ", "moonshotApiKey": "Moonshot API-ключ", "getMoonshotApiKey": "Получить Moonshot API-ключ", "moonshotBaseUrl": "Точка входа Moonshot", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 7348429c008..b4545c88266 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "DeepSeek API Anahtarı Al", "doubaoApiKey": "Doubao API Anahtarı", "getDoubaoApiKey": "Doubao API Anahtarı Al", - "qwenApiKey": "Qwen API Anahtarı", - "getQwenApiKey": "Qwen API Anahtarı Al", + "alibabaApiKey": "Alibaba API Anahtarı", + "getAlibabaApiKey": "Alibaba API Anahtarı Al", "moonshotApiKey": "Moonshot API Anahtarı", "getMoonshotApiKey": "Moonshot API Anahtarı Al", "moonshotBaseUrl": "Moonshot Giriş Noktası", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 67e271a79ba..594d3d2d456 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "Lấy khóa API DeepSeek", "doubaoApiKey": "Khóa API Doubao", "getDoubaoApiKey": "Lấy khóa API Doubao", - "qwenApiKey": "Khóa API Qwen", - "getQwenApiKey": "Lấy khóa API Qwen", + "alibabaApiKey": "Khóa API Qwen", + "getAlibabaApiKey": "Lấy khóa API Qwen", "moonshotApiKey": "Khóa API Moonshot", "getMoonshotApiKey": "Lấy khóa API Moonshot", "moonshotBaseUrl": "Điểm vào Moonshot", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 7f15e625eef..9b208c87826 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -392,8 +392,8 @@ "getDeepSeekApiKey": "获取 DeepSeek API 密钥", "doubaoApiKey": "豆包 API 密钥", "getDoubaoApiKey": "获取豆包 API 密钥", - "qwenApiKey": "Qwen API 密钥", - "getQwenApiKey": "获取 Qwen API 密钥", + "alibabaApiKey": "Alibaba API 密钥", + "getAlibabaApiKey": "获取 Alibaba API 密钥", "moonshotApiKey": "Moonshot API 密钥", "getMoonshotApiKey": "获取 Moonshot API 密钥", "moonshotBaseUrl": "Moonshot 服务站点", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 9c716a546d7..e65a49128e6 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -402,8 +402,8 @@ "getDeepSeekApiKey": "取得 DeepSeek API 金鑰", "doubaoApiKey": "豆包 API 金鑰", "getDoubaoApiKey": "取得豆包 API 金鑰", - "qwenApiKey": "Qwen API 金鑰", - "getQwenApiKey": "取得 Qwen API 金鑰", + "alibabaApiKey": "Alibaba API 金鑰", + "getAlibabaApiKey": "取得 Alibaba API 金鑰", "moonshotApiKey": "Moonshot API 金鑰", "getMoonshotApiKey": "取得 Moonshot API 金鑰", "moonshotBaseUrl": "Moonshot 服務端點",