From 548338d39e10f6c8431b25c3f2937191be5eb3c6 Mon Sep 17 00:00:00 2001 From: aicc Date: Thu, 6 Feb 2025 15:23:33 +0800 Subject: [PATCH] Add alibaba qwen models plus/max/coder-plus/turbo both stable and latest to use. (#1648) * add alibaba qwen-max qwen-plus qwen-turbo qwen-coder-plus stable/latest models * add alibaba qwen-max qwen-plus qwen-turbo qwen-coder-plus stable/latest models * Provide the api line choice for international user * Remove redundant code * Copy fixes * Create dry-socks-talk.md --------- Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> --- .changeset/dry-socks-talk.md | 5 ++ src/api/index.ts | 3 + src/api/providers/qwen.ts | 76 ++++++++++++++++ src/core/webview/ClineProvider.ts | 13 +++ src/shared/api.ts | 90 +++++++++++++++++++ .../src/components/settings/ApiOptions.tsx | 64 +++++++++++++ .../src/context/ExtensionStateContext.tsx | 1 + webview-ui/src/utils/validate.ts | 5 ++ 8 files changed, 257 insertions(+) create mode 100644 .changeset/dry-socks-talk.md create mode 100644 src/api/providers/qwen.ts diff --git a/.changeset/dry-socks-talk.md b/.changeset/dry-socks-talk.md new file mode 100644 index 0000000000..df4995488e --- /dev/null +++ b/.changeset/dry-socks-talk.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Add Alibaba qwen models plus/max/coder-plus/turbo diff --git a/src/api/index.ts b/src/api/index.ts index 5ed9e6de8c..680eb53232 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -11,6 +11,7 @@ import { GeminiHandler } from "./providers/gemini" import { OpenAiNativeHandler } from "./providers/openai-native" import { ApiStream } from "./transform/stream" import { DeepSeekHandler } from "./providers/deepseek" +import { QwenHandler } from "./providers/qwen" import { MistralHandler } from "./providers/mistral" import { VsCodeLmHandler } from "./providers/vscode-lm" import { LiteLlmHandler } from "./providers/litellm" @@ -47,6 +48,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new OpenAiNativeHandler(options) case "deepseek": return new DeepSeekHandler(options) + case "qwen": + return new QwenHandler(options) case "mistral": return new MistralHandler(options) case "vscode-lm": diff --git a/src/api/providers/qwen.ts b/src/api/providers/qwen.ts new file mode 100644 index 0000000000..9744fc1338 --- /dev/null +++ b/src/api/providers/qwen.ts @@ -0,0 +1,76 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" +import { ApiHandler } from "../" +import { ApiHandlerOptions, QwenModelId, ModelInfo, qwenDefaultModelId, qwenModels } from "../../shared/api" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { ApiStream } from "../transform/stream" + +export class QwenHandler implements ApiHandler { + private options: ApiHandlerOptions + private client: OpenAI + + constructor(options: ApiHandlerOptions) { + this.options = options + this.client = new OpenAI({ + baseURL: this.options.qwenApiLine || "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + apiKey: this.options.qwenApiKey, + }) + } + + getModel(): { id: QwenModelId; info: ModelInfo } { + const modelId = this.options.apiModelId + if (modelId && modelId in qwenModels) { + const id = modelId as QwenModelId + return { id, info: qwenModels[id] } + } + return { + id: qwenDefaultModelId, + info: qwenModels[qwenDefaultModelId], + } + } + + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const model = this.getModel() + let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ...convertToOpenAiMessages(messages), + ] + + const stream = await this.client.chat.completions.create({ + model: model.id, + max_completion_tokens: model.info.maxTokens, + messages: openAiMessages, + stream: true, + stream_options: { include_usage: true }, + }) + + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } + + if (delta && "reasoning_content" in delta && delta.reasoning_content) { + yield { + type: "reasoning", + reasoning: (delta.reasoning_content as string | undefined) || "", + } + } + + if (chunk.usage) { + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + // @ts-ignore-next-line + cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0, + // @ts-ignore-next-line + cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0, + } + } + } + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index a15b99ed05..415656ffe6 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -45,6 +45,7 @@ type SecretKey = | "geminiApiKey" | "openAiNativeApiKey" | "deepSeekApiKey" + | "qwenApiKey" | "mistralApiKey" | "authToken" | "authNonce" @@ -78,6 +79,7 @@ type GlobalStateKey = | "previousModeModelInfo" | "liteLlmBaseUrl" | "liteLlmModelId" + | "qwenApiLine" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -440,6 +442,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { geminiApiKey, openAiNativeApiKey, deepSeekApiKey, + qwenApiKey, mistralApiKey, azureApiVersion, openRouterModelId, @@ -447,6 +450,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { vsCodeLmModelSelector, liteLlmBaseUrl, liteLlmModelId, + qwenApiLine, } = message.apiConfiguration await this.updateGlobalState("apiProvider", apiProvider) await this.updateGlobalState("apiModelId", apiModelId) @@ -470,6 +474,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.storeSecret("geminiApiKey", geminiApiKey) await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey) await this.storeSecret("deepSeekApiKey", deepSeekApiKey) + await this.storeSecret("qwenApiKey", qwenApiKey) await this.storeSecret("mistralApiKey", mistralApiKey) await this.updateGlobalState("azureApiVersion", azureApiVersion) await this.updateGlobalState("openRouterModelId", openRouterModelId) @@ -477,6 +482,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector) await this.updateGlobalState("liteLlmBaseUrl", liteLlmBaseUrl) await this.updateGlobalState("liteLlmModelId", liteLlmModelId) + await this.updateGlobalState("qwenApiLine", qwenApiLine) if (this.cline) { this.cline.api = buildApiHandler(message.apiConfiguration) } @@ -1365,6 +1371,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { geminiApiKey, openAiNativeApiKey, deepSeekApiKey, + qwenApiKey, mistralApiKey, azureApiVersion, openRouterModelId, @@ -1383,6 +1390,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { previousModeApiProvider, previousModeModelId, previousModeModelInfo, + qwenApiLine, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -1406,6 +1414,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getSecret("geminiApiKey") as Promise, this.getSecret("openAiNativeApiKey") as Promise, this.getSecret("deepSeekApiKey") as Promise, + this.getSecret("qwenApiKey") as Promise, this.getSecret("mistralApiKey") as Promise, this.getGlobalState("azureApiVersion") as Promise, this.getGlobalState("openRouterModelId") as Promise, @@ -1424,6 +1433,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("previousModeApiProvider") as Promise, this.getGlobalState("previousModeModelId") as Promise, this.getGlobalState("previousModeModelInfo") as Promise, + this.getGlobalState("qwenApiLine") as Promise, ]) let apiProvider: ApiProvider @@ -1464,6 +1474,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { geminiApiKey, openAiNativeApiKey, deepSeekApiKey, + qwenApiKey, + qwenApiLine, mistralApiKey, azureApiVersion, openRouterModelId, @@ -1559,6 +1571,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { "geminiApiKey", "openAiNativeApiKey", "deepSeekApiKey", + "qwenApiKey", "mistralApiKey", "authToken", ] diff --git a/src/shared/api.ts b/src/shared/api.ts index d0651b0419..24f93e96cf 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -9,6 +9,7 @@ export type ApiProvider = | "gemini" | "openai-native" | "deepseek" + | "qwen" | "mistral" | "vscode-lm" | "litellm" @@ -39,9 +40,11 @@ export interface ApiHandlerOptions { geminiApiKey?: string openAiNativeApiKey?: string deepSeekApiKey?: string + qwenApiKey?: string mistralApiKey?: string azureApiVersion?: string vsCodeLmModelSelector?: any + qwenApiLine?: string } export type ApiConfiguration = ApiHandlerOptions & { @@ -432,6 +435,93 @@ export const deepSeekModels = { }, } as const satisfies Record +// Qwen +// https://bailian.console.aliyun.com/ +export type QwenModelId = keyof typeof qwenModels +export const qwenDefaultModelId: QwenModelId = "qwen-coder-plus-latest" +export const qwenModels = { + "qwen-coder-plus-latest": { + maxTokens: 129_024, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0035, + outputPrice: 0.007, + cacheWritesPrice: 0.0035, + cacheReadsPrice: 0.007, + }, + "qwen-plus-latest": { + maxTokens: 129_024, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0008, + outputPrice: 0.002, + cacheWritesPrice: 0.0004, + cacheReadsPrice: 0.001, + }, + "qwen-turbo-latest": { + maxTokens: 1_000_000, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0003, + outputPrice: 0.0006, + cacheWritesPrice: 0.00015, + cacheReadsPrice: 0.0003, + }, + "qwen-max-latest": { + maxTokens: 30_720, + contextWindow: 32_768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0112, + outputPrice: 0.0448, + cacheWritesPrice: 0.0056, + cacheReadsPrice: 0.0224, + }, + "qwen-coder-plus": { + maxTokens: 129_024, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0035, + outputPrice: 0.007, + cacheWritesPrice: 0.0035, + cacheReadsPrice: 0.007, + }, + "qwen-plus": { + maxTokens: 129_024, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0008, + outputPrice: 0.002, + cacheWritesPrice: 0.0004, + cacheReadsPrice: 0.001, + }, + "qwen-turbo": { + maxTokens: 1_000_000, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0003, + outputPrice: 0.0006, + cacheWritesPrice: 0.00015, + cacheReadsPrice: 0.0003, + }, + "qwen-max": { + maxTokens: 30_720, + contextWindow: 32_768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0112, + outputPrice: 0.0448, + cacheWritesPrice: 0.0056, + cacheReadsPrice: 0.0224, + }, +} as const satisfies Record + // Mistral // https://docs.mistral.ai/getting-started/models/models_overview/ export type MistralModelId = keyof typeof mistralModels diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 84018b9efd..cbbf18198d 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -20,6 +20,8 @@ import { bedrockModels, deepSeekDefaultModelId, deepSeekModels, + qwenDefaultModelId, + qwenModels, geminiDefaultModelId, geminiModels, mistralDefaultModelId, @@ -179,6 +181,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is Anthropic Google Gemini DeepSeek + Qwen Mistral GCP Vertex AI AWS Bedrock @@ -310,6 +313,64 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is )} + {selectedProvider === "qwen" && ( +
+ + + + China API + + International API + + + +

+ Please select the appropriate API interface based on your location. If you are in China, choose the China + API interface. Otherwise, choose the International API interface. +

+ + Qwen API Key + +

+ This key is stored locally and only used to make API requests from this extension. + {!apiConfiguration?.qwenApiKey && ( + + You can get a Qwen API key by signing up here. + + )} +

+
+ )} + {selectedProvider === "mistral" && (
@@ -1068,6 +1130,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration): return getProviderData(openAiNativeModels, openAiNativeDefaultModelId) case "deepseek": return getProviderData(deepSeekModels, deepSeekDefaultModelId) + case "qwen": + return getProviderData(qwenModels, qwenDefaultModelId) case "mistral": return getProviderData(mistralModels, mistralDefaultModelId) case "openrouter": diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 626e9e6606..99a8cca3f9 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -67,6 +67,7 @@ export const ExtensionStateContextProvider: React.FC<{ config.geminiApiKey, config.openAiNativeApiKey, config.deepSeekApiKey, + config.qwenApiKey, config.mistralApiKey, config.vsCodeLmModelSelector, ].some((key) => key !== undefined) diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index beafc65572..4617fb2c70 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -38,6 +38,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s return "You must provide a valid API key or choose a different provider." } break + case "qwen": + if (!apiConfiguration.qwenApiKey) { + return "You must provide a valid API key or choose a different provider." + } + break case "mistral": if (!apiConfiguration.mistralApiKey) { return "You must provide a valid API key or choose a different provider."