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>
This commit is contained in:
aicc 2025-02-06 15:23:33 +08:00 committed by GitHub
parent c88b5c67d5
commit 548338d39e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 257 additions and 0 deletions

View file

@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add Alibaba qwen models plus/max/coder-plus/turbo

View file

@ -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":

76
src/api/providers/qwen.ts Normal file
View file

@ -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,
}
}
}
}
}

View file

@ -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<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@ -1406,6 +1414,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getSecret("geminiApiKey") as Promise<string | undefined>,
this.getSecret("openAiNativeApiKey") as Promise<string | undefined>,
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
this.getSecret("qwenApiKey") as Promise<string | undefined>,
this.getSecret("mistralApiKey") as Promise<string | undefined>,
this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
this.getGlobalState("openRouterModelId") as Promise<string | undefined>,
@ -1424,6 +1433,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("previousModeApiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("previousModeModelId") as Promise<string | undefined>,
this.getGlobalState("previousModeModelInfo") as Promise<ModelInfo | undefined>,
this.getGlobalState("qwenApiLine") as Promise<string | undefined>,
])
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",
]

View file

@ -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<string, ModelInfo>
// 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<string, ModelInfo>
// Mistral
// https://docs.mistral.ai/getting-started/models/models_overview/
export type MistralModelId = keyof typeof mistralModels

View file

@ -20,6 +20,8 @@ import {
bedrockModels,
deepSeekDefaultModelId,
deepSeekModels,
qwenDefaultModelId,
qwenModels,
geminiDefaultModelId,
geminiModels,
mistralDefaultModelId,
@ -179,6 +181,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
<VSCodeOption value="deepseek">DeepSeek</VSCodeOption>
<VSCodeOption value="qwen">Qwen</VSCodeOption>
<VSCodeOption value="mistral">Mistral</VSCodeOption>
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
@ -310,6 +313,64 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</div>
)}
{selectedProvider === "qwen" && (
<div>
<DropdownContainer className="dropdown-container">
<label htmlFor="qwen-line-provider">
<span style={{ fontWeight: 500, marginTop: 5 }}>Alibaba API Line</span>
</label>
<VSCodeDropdown
id="qwen-line-provider"
value={apiConfiguration?.qwenApiLine || ""}
onChange={handleInputChange("qwenApiLine")}
style={{
minWidth: 130,
position: "relative",
}}>
<VSCodeOption value="https://dashscope.aliyuncs.com/compatible-mode/v1">China API</VSCodeOption>
<VSCodeOption value="https://dashscope-intl.aliyuncs.com/compatible-mode/v1">
International API
</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
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.
</p>
<VSCodeTextField
value={apiConfiguration?.qwenApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("qwenApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Qwen API Key</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
This key is stored locally and only used to make API requests from this extension.
{!apiConfiguration?.qwenApiKey && (
<VSCodeLink
href="https://bailian.console.aliyun.com/"
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a Qwen API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)}
{selectedProvider === "mistral" && (
<div>
<VSCodeTextField
@ -861,6 +922,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{selectedProvider === "gemini" && createDropdown(geminiModels)}
{selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
{selectedProvider === "qwen" && createDropdown(qwenModels)}
{selectedProvider === "mistral" && createDropdown(mistralModels)}
</DropdownContainer>
@ -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":

View file

@ -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)

View file

@ -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."