diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index c13319a956..813f36aa7f 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -18,6 +18,7 @@ import { mistralModels, moonshotModels, openAiNativeModels, + qwenCodeModels, rooModels, sambaNovaModels, vertexModels, @@ -63,6 +64,7 @@ export const providerNames = [ "fireworks", "featherless", "io-intelligence", + "qwen-code", "roo", ] as const @@ -291,6 +293,10 @@ const sambaNovaSchema = apiModelIdProviderModelSchema.extend({ sambaNovaApiKey: z.string().optional(), }) +const qwenCodeSchema = apiModelIdProviderModelSchema.extend({ + qwenCodeOAuthPath: z.string().optional(), +}) + const zaiSchema = apiModelIdProviderModelSchema.extend({ zaiApiKey: z.string().optional(), zaiApiLine: z.union([z.literal("china"), z.literal("international")]).optional(), @@ -346,6 +352,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv litellmSchema.merge(z.object({ apiProvider: z.literal("litellm") })), cerebrasSchema.merge(z.object({ apiProvider: z.literal("cerebras") })), sambaNovaSchema.merge(z.object({ apiProvider: z.literal("sambanova") })), + qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })), zaiSchema.merge(z.object({ apiProvider: z.literal("zai") })), fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })), featherlessSchema.merge(z.object({ apiProvider: z.literal("featherless") })), @@ -384,6 +391,7 @@ export const providerSettingsSchema = z.object({ ...litellmSchema.shape, ...cerebrasSchema.shape, ...sambaNovaSchema.shape, + ...qwenCodeSchema.shape, ...zaiSchema.shape, ...fireworksSchema.shape, ...featherlessSchema.shape, @@ -504,6 +512,11 @@ export const MODELS_BY_PROVIDER: Record< label: "OpenAI", models: Object.keys(openAiNativeModels), }, + "qwen-code": { + id: "qwen-code", + label: "Qwen Code", + models: Object.keys(qwenCodeModels), + }, roo: { id: "roo", label: "Roo", models: Object.keys(rooModels) }, sambanova: { id: "sambanova", diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 8ca9c2c9b2..42ec06abff 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -22,6 +22,7 @@ export * from "./openrouter.js" export * from "./requesty.js" export * from "./roo.js" export * from "./sambanova.js" +export * from "./qwen-code.js" export * from "./unbound.js" export * from "./vertex.js" export * from "./vscode-llm.js" diff --git a/packages/types/src/providers/qwen-code.ts b/packages/types/src/providers/qwen-code.ts new file mode 100644 index 0000000000..323166f700 --- /dev/null +++ b/packages/types/src/providers/qwen-code.ts @@ -0,0 +1,44 @@ +import type { ModelInfo } from "../model.js" +import type { ProviderName } from "../provider-settings.js" + +export const qwenCodeModels = { + "qwen3-coder-plus": { + id: "qwen3-coder-plus", + name: "Qwen3 Coder Plus", + provider: "qwen-code" as ProviderName, + contextWindow: 1000000, + maxTokens: 65536, + supportsPromptCache: false, + }, + "qwen3-coder-flash": { + id: "qwen3-coder-flash", + name: "Qwen3 Coder Flash", + provider: "qwen-code" as ProviderName, + contextWindow: 1000000, + maxTokens: 65536, + supportsPromptCache: false, + }, +} as const + +export type QwenCodeModelId = keyof typeof qwenCodeModels + +export const qwenCodeDefaultModelId: QwenCodeModelId = "qwen3-coder-plus" + +export const isQwenCodeModel = (modelId: string): modelId is QwenCodeModelId => { + return modelId in qwenCodeModels +} + +export const getQwenCodeModelInfo = (modelId: string): ModelInfo => { + if (isQwenCodeModel(modelId)) { + return qwenCodeModels[modelId] + } + // Fallback to a default or throw an error + return qwenCodeModels[qwenCodeDefaultModelId] +} + +export type QwenCodeProvider = { + id: "qwen-code" + apiKey?: string + baseUrl?: string + model: QwenCodeModelId +} diff --git a/src/api/index.ts b/src/api/index.ts index 48a0a89ec5..cfdecf9359 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -34,6 +34,7 @@ import { IOIntelligenceHandler, DoubaoHandler, ZAiHandler, + QwenCodeHandler, FireworksHandler, RooHandler, FeatherlessHandler, @@ -138,6 +139,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new SambaNovaHandler(options) case "zai": return new ZAiHandler(options) + case "qwen-code": + return new QwenCodeHandler(options) case "fireworks": return new FireworksHandler(options) case "io-intelligence": diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index d256fbbe55..7f05ebf996 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -28,6 +28,7 @@ export { VertexHandler } from "./vertex" export { VsCodeLmHandler } from "./vscode-lm" export { XAIHandler } from "./xai" export { ZAiHandler } from "./zai" +export { QwenCodeHandler } from "./qwen-code" export { FireworksHandler } from "./fireworks" export { RooHandler } from "./roo" export { FeatherlessHandler } from "./featherless" diff --git a/src/api/providers/qwen-code.ts b/src/api/providers/qwen-code.ts new file mode 100644 index 0000000000..a6f7c44d77 --- /dev/null +++ b/src/api/providers/qwen-code.ts @@ -0,0 +1,294 @@ +import type { Anthropic } from "@anthropic-ai/sdk" +import { promises as fs } from "node:fs" +import * as os from "os" +import * as path from "path" +import OpenAI from "openai" + +import { XmlMatcher } from "../../utils/xml-matcher" +import type { ModelInfo, QwenCodeModelId } from "@roo-code/types" +import { qwenCodeDefaultModelId, qwenCodeModels } from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" +import { t } from "../../i18n" +import { convertToOpenAiMessages } from "../transform/openai-format" +import type { ApiStream, ApiStreamUsageChunk } from "../transform/stream" +import { getModelParams } from "../transform/model-params" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import { BaseProvider } from "./base-provider" +import { DEFAULT_HEADERS } from "./constants" + +// --- Constants from qwenOAuth2.js --- + +const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai" +const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token` +const QWEN_OAUTH_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56" +const QWEN_DIR = ".qwen" +const QWEN_CREDENTIAL_FILENAME = "oauth_creds.json" + +interface QwenOAuthCredentials { + access_token: string + refresh_token: string + token_type: string + expiry_date: number + resource_url?: string +} + +function getQwenCachedCredentialPath(customPath?: string): string { + if (customPath) { + return customPath + } + return path.join(os.homedir(), QWEN_DIR, QWEN_CREDENTIAL_FILENAME) +} + +function objectToUrlEncoded(data: Record): string { + return Object.keys(data) + .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`) + .join("&") +} + +export class QwenCodeHandler extends BaseProvider implements SingleCompletionHandler { + protected options: ApiHandlerOptions + private credentials: QwenOAuthCredentials | null = null + private client: OpenAI + + constructor(options: ApiHandlerOptions) { + super() + this.options = options + // Create the client instance once in the constructor. + // The API key will be updated dynamically via ensureAuthenticated. + this.client = new OpenAI({ + apiKey: "dummy-key-will-be-replaced", + baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", // A default base URL + defaultHeaders: DEFAULT_HEADERS, + }) + } + + private async loadCachedQwenCredentials(): Promise { + try { + const keyFile = getQwenCachedCredentialPath(this.options.qwenCodeOAuthPath) + const credsStr = await fs.readFile(keyFile, "utf-8") + return JSON.parse(credsStr) + } catch (error) { + console.error( + `Error reading or parsing credentials file at ${getQwenCachedCredentialPath(this.options.qwenCodeOAuthPath)}`, + ) + throw new Error(t("common:errors.qwenCode.oauthLoadFailed", { error })) + } + } + + private async refreshAccessToken(credentials: QwenOAuthCredentials): Promise { + if (!credentials.refresh_token) { + throw new Error("No refresh token available in credentials.") + } + + const bodyData = { + grant_type: "refresh_token", + refresh_token: credentials.refresh_token, + client_id: QWEN_OAUTH_CLIENT_ID, + } + + const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: objectToUrlEncoded(bodyData), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Token refresh failed: ${response.status} ${response.statusText}. Response: ${errorText}`) + } + + const tokenData = await response.json() + + if (tokenData.error) { + throw new Error(`Token refresh failed: ${tokenData.error} - ${tokenData.error_description}`) + } + + const newCredentials = { + ...credentials, + access_token: tokenData.access_token, + token_type: tokenData.token_type, + refresh_token: tokenData.refresh_token || credentials.refresh_token, + expiry_date: Date.now() + tokenData.expires_in * 1000, + } + + const filePath = getQwenCachedCredentialPath(this.options.qwenCodeOAuthPath) + await fs.writeFile(filePath, JSON.stringify(newCredentials, null, 2)) + + return newCredentials + } + + private isTokenValid(credentials: QwenOAuthCredentials): boolean { + const TOKEN_REFRESH_BUFFER_MS = 30 * 1000 // 30s buffer + if (!credentials.expiry_date) { + return false + } + return Date.now() < credentials.expiry_date - TOKEN_REFRESH_BUFFER_MS + } + + private async ensureAuthenticated(): Promise { + if (!this.credentials) { + this.credentials = await this.loadCachedQwenCredentials() + } + + if (!this.isTokenValid(this.credentials)) { + this.credentials = await this.refreshAccessToken(this.credentials) + } + + // After authentication, just update the apiKey and baseURL on the existing client. + this.client.apiKey = this.credentials.access_token + this.client.baseURL = this.getBaseUrl(this.credentials) + } + + private getBaseUrl(creds: QwenOAuthCredentials): string { + let baseUrl = creds.resource_url || "https://dashscope.aliyuncs.com/compatible-mode/v1" + if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) { + baseUrl = `https://${baseUrl}` + } + return baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1` + } + + private async callApiWithRetry(apiCall: () => Promise): Promise { + try { + return await apiCall() + } catch (error: any) { + if (error.status === 401) { + this.credentials = await this.refreshAccessToken(this.credentials!) + // Just update the key, don't re-create the client + this.client.apiKey = this.credentials.access_token + this.client.baseURL = this.getBaseUrl(this.credentials) + return await apiCall() + } else { + throw error + } + } + } + + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + await this.ensureAuthenticated() + + const { id: modelId, info: modelInfo } = this.getModel() + + const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = { + role: "system", + content: systemPrompt, + } + + const convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)] + + const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { + model: modelId, + temperature: this.options.modelTemperature ?? 0, + messages: convertedMessages, + stream: true, + stream_options: { include_usage: true }, + } + + this.addMaxTokensIfNeeded(requestOptions, modelInfo) + + const stream = await this.callApiWithRetry(() => this.client!.chat.completions.create(requestOptions)) + + const matcher = new XmlMatcher( + "think", + (chunk) => + ({ + type: chunk.matched ? "reasoning" : "text", + text: chunk.data, + }) as const, + ) + + let lastUsage: any + let fullContent = "" + + for await (const apiChunk of stream) { + const delta = apiChunk.choices[0]?.delta ?? {} + + if (delta.content) { + let newText = delta.content + if (newText.startsWith(fullContent)) { + newText = newText.substring(fullContent.length) + } + fullContent = delta.content + + if (newText) { + for (const processedChunk of matcher.update(newText)) { + yield processedChunk + } + } + } + + if ("reasoning_content" in delta && delta.reasoning_content) { + yield { + type: "reasoning", + text: (delta.reasoning_content as string | undefined) || "", + } + } + if (apiChunk.usage) { + lastUsage = apiChunk.usage + } + } + + for (const chunk of matcher.final()) { + yield chunk + } + + if (lastUsage) { + yield this.processUsageMetrics(lastUsage, modelInfo) + } + } + + async completePrompt(prompt: string): Promise { + await this.ensureAuthenticated() + + const { id: modelId, info: modelInfo } = this.getModel() + + const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = { + model: modelId, + messages: [{ role: "user", content: prompt }], + } + + this.addMaxTokensIfNeeded(requestOptions, modelInfo) + + const response = await this.callApiWithRetry(() => this.client!.chat.completions.create(requestOptions)) + + return response.choices[0]?.message.content || "" + } + + override getModel() { + const modelId = this.options.apiModelId + const id = modelId && modelId in qwenCodeModels ? (modelId as QwenCodeModelId) : qwenCodeDefaultModelId + const info: ModelInfo = qwenCodeModels[id] + const params = getModelParams({ format: "openai", modelId: id, model: info, settings: this.options }) + + return { id, info, ...params } + } + + protected processUsageMetrics(usage: any, _modelInfo?: ModelInfo): ApiStreamUsageChunk { + return { + type: "usage", + inputTokens: usage?.prompt_tokens || 0, + outputTokens: usage?.completion_tokens || 0, + } + } + + /** + * Adds max_completion_tokens to the request body if needed based on provider configuration + */ + private addMaxTokensIfNeeded( + requestOptions: + | OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming + | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming, + modelInfo: ModelInfo, + ): void { + if (this.options.includeMaxTokens === true) { + requestOptions.max_completion_tokens = this.options.modelMaxTokens || modelInfo.maxTokens + } + } +} diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 6235593f7e..9cdc9a7a07 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -107,6 +107,9 @@ "roo": { "authenticationRequired": "El proveïdor Roo requereix autenticació al núvol. Si us plau, inicieu sessió a Roo Code Cloud." }, + "qwenCode": { + "oauthLoadFailed": "No s'han pogut carregar les credencials OAuth de QwenCode: {{error}}" + }, "mode_import_failed": "Ha fallat la importació del mode: {{error}}" }, "warnings": { diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 6819b27d73..09054445df 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Roo-Anbieter erfordert Cloud-Authentifizierung. Bitte melde dich bei Roo Code Cloud an." + }, + "qwenCode": { + "oauthLoadFailed": "Fehler beim Laden der QwenCode OAuth-Anmeldedaten: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 696ecb44d4..20f5e41607 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Roo provider requires cloud authentication. Please sign in to Roo Code Cloud." + }, + "qwenCode": { + "oauthLoadFailed": "Failed to load QwenCode OAuth credentials: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index c1b399b84f..b00c3af11a 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "El proveedor Roo requiere autenticación en la nube. Por favor, inicia sesión en Roo Code Cloud." + }, + "qwenCode": { + "oauthLoadFailed": "Error al cargar las credenciales OAuth de QwenCode: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 682e12e224..6fd538a783 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Le fournisseur Roo nécessite une authentification cloud. Veuillez vous connecter à Roo Code Cloud." + }, + "qwenCode": { + "oauthLoadFailed": "Échec du chargement des identifiants OAuth QwenCode : {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 05e0a622cc..8d538fbefb 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Roo प्रदाता को क्लाउड प्रमाणीकरण की आवश्यकता है। कृपया Roo Code Cloud में साइन इन करें।" + }, + "qwenCode": { + "oauthLoadFailed": "QwenCode OAuth क्रेडेंशियल लोड करने में विफल: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 1595b795cf..4e1dc2d348 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Penyedia Roo memerlukan autentikasi cloud. Silakan masuk ke Roo Code Cloud." + }, + "qwenCode": { + "oauthLoadFailed": "Gagal memuat kredensial OAuth QwenCode: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 73f4d47788..1472e83917 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Il provider Roo richiede l'autenticazione cloud. Accedi a Roo Code Cloud." + }, + "qwenCode": { + "oauthLoadFailed": "Impossibile caricare le credenziali OAuth QwenCode: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index cb55c7bf0b..f122343c5e 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Rooプロバイダーはクラウド認証が必要です。Roo Code Cloudにサインインしてください。" + }, + "qwenCode": { + "oauthLoadFailed": "QwenCode OAuth認証情報の読み込みに失敗しました: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 9bb61b6563..78848369bb 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Roo 제공업체는 클라우드 인증이 필요합니다. Roo Code Cloud에 로그인하세요." + }, + "qwenCode": { + "oauthLoadFailed": "QwenCode OAuth 자격 증명 로드 실패: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index fb2fcec9f9..06ae36f2fc 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Roo provider vereist cloud authenticatie. Log in bij Roo Code Cloud." + }, + "qwenCode": { + "oauthLoadFailed": "Kan QwenCode OAuth-referenties niet laden: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 2a6fee3e23..1290ed4128 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Dostawca Roo wymaga uwierzytelnienia w chmurze. Zaloguj się do Roo Code Cloud." + }, + "qwenCode": { + "oauthLoadFailed": "Nie udało się załadować danych uwierzytelniających OAuth QwenCode: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 83d960ad2d..7c75a2a7fd 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -107,6 +107,9 @@ }, "roo": { "authenticationRequired": "O provedor Roo requer autenticação na nuvem. Faça login no Roo Code Cloud." + }, + "qwenCode": { + "oauthLoadFailed": "Falha ao carregar credenciais OAuth do QwenCode: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 9c37cfe3ed..01e30f9296 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Провайдер Roo требует облачной аутентификации. Войдите в Roo Code Cloud." + }, + "qwenCode": { + "oauthLoadFailed": "Не удалось загрузить учетные данные OAuth QwenCode: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index d99008755e..121df15bf8 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Roo sağlayıcısı bulut kimlik doğrulaması gerektirir. Lütfen Roo Code Cloud'a giriş yapın." + }, + "qwenCode": { + "oauthLoadFailed": "QwenCode OAuth kimlik bilgileri yüklenemedi: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index d29525cc03..7f67c485a4 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -103,6 +103,9 @@ }, "roo": { "authenticationRequired": "Nhà cung cấp Roo yêu cầu xác thực đám mây. Vui lòng đăng nhập vào Roo Code Cloud." + }, + "qwenCode": { + "oauthLoadFailed": "Không thể tải thông tin xác thực OAuth QwenCode: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index fc0386c95d..dd90e26e4f 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -108,6 +108,9 @@ }, "roo": { "authenticationRequired": "Roo 提供商需要云认证。请登录 Roo Code Cloud。" + }, + "qwenCode": { + "oauthLoadFailed": "加载 QwenCode OAuth 凭据失败:{{error}}" } }, "warnings": { diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 753463b9f5..902a8d47ae 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -103,6 +103,9 @@ "roo": { "authenticationRequired": "Roo 提供者需要雲端認證。請登入 Roo Code Cloud。" }, + "qwenCode": { + "oauthLoadFailed": "載入 QwenCode OAuth 憑證失敗:{{error}}" + }, "mode_import_failed": "匯入模式失敗:{{error}}" }, "warnings": { diff --git a/src/shared/checkExistApiConfig.ts b/src/shared/checkExistApiConfig.ts index 8acb88ed3f..1124398e72 100644 --- a/src/shared/checkExistApiConfig.ts +++ b/src/shared/checkExistApiConfig.ts @@ -6,7 +6,7 @@ export function checkExistKey(config: ProviderSettings | undefined) { } // Special case for human-relay, fake-ai, claude-code, and roo providers which don't need any configuration. - if (config.apiProvider && ["human-relay", "fake-ai", "claude-code", "roo"].includes(config.apiProvider)) { + if (config.apiProvider && ["human-relay", "fake-ai", "claude-code", "roo","qwen-code"].includes(config.apiProvider)) { return true } diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 787a95b166..80136ec4df 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -34,6 +34,7 @@ import { featherlessDefaultModelId, ioIntelligenceDefaultModelId, rooDefaultModelId, + qwenCodeDefaultModelId, } from "@roo-code/types" import { vscode } from "@src/utils/vscode" @@ -89,6 +90,7 @@ import { ZAi, Fireworks, Featherless, + QwenCode, } from "./providers" import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants" @@ -321,6 +323,7 @@ const ApiOptions = ({ bedrock: { field: "apiModelId", default: bedrockDefaultModelId }, vertex: { field: "apiModelId", default: vertexDefaultModelId }, sambanova: { field: "apiModelId", default: sambaNovaDefaultModelId }, + "qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId }, zai: { field: "apiModelId", default: @@ -543,6 +546,10 @@ const ApiOptions = ({ )} + {selectedProvider === "qwen-code" && ( + + )} + {selectedProvider === "litellm" && ( void +} + +export const QwenCode = ({ apiConfiguration, setApiConfigurationField }: QwenCodeProps) => { + 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.qwenCode.oauthPathDescription")} +
+ +
+ {t("settings:providers.qwenCode.description")} +
+ +
+ {t("settings:providers.qwenCode.instructions")} +
+ + + {t("settings:providers.qwenCode.setupLink")} + + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index eff33e1298..ce029bf86a 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -24,6 +24,7 @@ export { Vertex } from "./Vertex" export { VSCodeLM } from "./VSCodeLM" export { XAI } from "./XAI" export { ZAi } from "./ZAi" +export { QwenCode } from "./QwenCode" export { LiteLLM } from "./LiteLLM" export { Fireworks } from "./Fireworks" export { Featherless } from "./Featherless" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 75a4a968ad..36ea6c83e8 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -53,6 +53,8 @@ import { rooDefaultModelId, rooModels, BEDROCK_CLAUDE_SONNET_4_MODEL_ID, + qwenCodeModels, + qwenCodeDefaultModelId, } from "@roo-code/types" import type { ModelRecord, RouterModels } from "@roo/api" @@ -310,6 +312,11 @@ function getSelectedModel({ const info = rooModels[id as keyof typeof rooModels] return { id, info } } + case "qwen-code": { + const id = apiConfiguration.apiModelId ?? qwenCodeDefaultModelId + const info = qwenCodeModels[id as keyof typeof qwenCodeModels] + return { id, info } + } // case "anthropic": // case "human-relay": // case "fake-ai": diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index d9c7ce7ee2..6c4925dbb4 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -390,6 +390,13 @@ "learnMore": "Més informació sobre l'encaminament de proveïdors" } }, + "qwenCode": { + "oauthPath": "Ruta de credencials OAuth (opcional)", + "oauthPathDescription": "Ruta al fitxer de credencials OAuth. Deixeu-ho buit per utilitzar la ubicació predeterminada (~/.qwen/oauth_creds.json).", + "description": "Aquest proveïdor utilitza autenticació OAuth del servei Qwen i no requereix claus d'API.", + "instructions": "Si us plau, seguiu la documentació oficial per obtenir el fitxer d'autorització i col·loqueu-lo a la ruta especificada.", + "setupLink": "Documentació oficial de Qwen" + }, "customModel": { "capabilities": "Configureu les capacitats i preus per al vostre model personalitzat compatible amb OpenAI. Tingueu cura en especificar les capacitats del model, ja que poden afectar com funciona Roo Code.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 7f09401e57..e9a1129f9a 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -390,6 +390,13 @@ "learnMore": "Mehr über Anbieter-Routing erfahren" } }, + "qwenCode": { + "oauthPath": "OAuth-Anmeldedaten-Pfad (optional)", + "oauthPathDescription": "Pfad zur OAuth-Anmeldedatei. Leer lassen, um den Standardspeicherort zu verwenden (~/.qwen/oauth_creds.json).", + "description": "Dieser Anbieter verwendet OAuth-Authentifizierung vom Qwen-Dienst und benötigt keine API-Schlüssel.", + "instructions": "Bitte folgen Sie der offiziellen Dokumentation, um die Autorisierungsdatei zu erhalten und sie im angegebenen Pfad zu platzieren.", + "setupLink": "Qwen Offizielle Dokumentation" + }, "customModel": { "capabilities": "Konfiguriere die Fähigkeiten und Preise für dein benutzerdefiniertes OpenAI-kompatibles Modell. Sei vorsichtig bei der Angabe der Modellfähigkeiten, da diese beeinflussen können, wie Roo Code funktioniert.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index d18a3bbd5e..5099d76afe 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -389,6 +389,13 @@ "learnMore": "Learn more about provider routing" } }, + "qwenCode": { + "oauthPath": "OAuth Credentials Path (optional)", + "oauthPathDescription": "Path to the OAuth credentials file. Leave empty to use the default location (~/.qwen/oauth_creds.json).", + "description": "This provider uses OAuth authentication from the Qwen service and does not require API keys.", + "instructions": "Please follow the official documentation to obtain the authorization file and place it in the specified path.", + "setupLink": "Qwen Official Documentation" + }, "customModel": { "capabilities": "Configure the capabilities and pricing for your custom OpenAI-compatible model. Be careful when specifying the model capabilities, as they can affect how Roo Code performs.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index ec9795d8b0..f227207ce9 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -390,6 +390,13 @@ "learnMore": "Más información sobre el enrutamiento de proveedores" } }, + "qwenCode": { + "oauthPath": "Ruta de credenciales OAuth (opcional)", + "oauthPathDescription": "Ruta al archivo de credenciales OAuth. Déjelo vacío para usar la ubicación predeterminada (~/.qwen/oauth_creds.json).", + "description": "Este proveedor utiliza autenticación OAuth del servicio Qwen y no requiere claves de API.", + "instructions": "Por favor, siga la documentación oficial para obtener el archivo de autorización y colóquelo en la ruta especificada.", + "setupLink": "Documentación oficial de Qwen" + }, "customModel": { "capabilities": "Configure las capacidades y precios para su modelo personalizado compatible con OpenAI. Tenga cuidado al especificar las capacidades del modelo, ya que pueden afectar cómo funciona Roo Code.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 68230b1a60..5fcecb9624 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -390,6 +390,13 @@ "learnMore": "En savoir plus sur le routage des fournisseurs" } }, + "qwenCode": { + "oauthPath": "Chemin des identifiants OAuth (optionnel)", + "oauthPathDescription": "Chemin vers le fichier d'identifiants OAuth. Laissez vide pour utiliser l'emplacement par défaut (~/.qwen/oauth_creds.json).", + "description": "Ce fournisseur utilise l'authentification OAuth du service Qwen et ne nécessite pas de clés API.", + "instructions": "Veuillez suivre la documentation officielle pour obtenir le fichier d'autorisation et le placer dans le chemin spécifié.", + "setupLink": "Documentation officielle Qwen" + }, "customModel": { "capabilities": "Configurez les capacités et les prix pour votre modèle personnalisé compatible OpenAI. Soyez prudent lors de la spécification des capacités du modèle, car elles peuvent affecter le fonctionnement de Roo Code.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index f98f7e6510..0e811302c4 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -390,6 +390,13 @@ "learnMore": "प्रदाता रूटिंग के बारे में अधिक जानें" } }, + "qwenCode": { + "oauthPath": "OAuth क्रेडेंशियल पथ (वैकल्पिक)", + "oauthPathDescription": "OAuth क्रेडेंशियल फ़ाइल का पथ। डिफ़ॉल्ट स्थान (~/.qwen/oauth_creds.json) का उपयोग करने के लिए खाली छोड़ें।", + "description": "यह प्रदाता Qwen सेवा से OAuth प्रमाणीकरण का उपयोग करता है और API कुंजियों की आवश्यकता नहीं है।", + "instructions": "कृपया आधिकारिक दस्तावेज़ीकरण का पालन करें और प्राधिकरण फ़ाइल प्राप्त करें और इसे निर्दिष्ट पथ में रखें।", + "setupLink": "Qwen आधिकारिक दस्तावेज़ीकरण" + }, "customModel": { "capabilities": "अपने कस्टम OpenAI-संगत मॉडल के लिए क्षमताओं और मूल्य निर्धारण को कॉन्फ़िगर करें। मॉडल क्षमताओं को निर्दिष्ट करते समय सावधान रहें, क्योंकि वे Roo Code के प्रदर्शन को प्रभावित कर सकती हैं।", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 748bf198eb..2b63f487b0 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -394,6 +394,13 @@ "learnMore": "Pelajari lebih lanjut tentang provider routing" } }, + "qwenCode": { + "oauthPath": "Jalur Kredensial OAuth (opsional)", + "oauthPathDescription": "Jalur ke file kredensial OAuth. Biarkan kosong untuk menggunakan lokasi default (~/.qwen/oauth_creds.json).", + "description": "Penyedia ini menggunakan autentikasi OAuth dari layanan Qwen dan tidak memerlukan kunci API.", + "instructions": "Silakan ikuti dokumentasi resmi untuk mendapatkan file otorisasi dan tempatkan di jalur yang ditentukan.", + "setupLink": "Dokumentasi Resmi Qwen" + }, "customModel": { "capabilities": "Konfigurasi kemampuan dan harga untuk model kustom yang kompatibel dengan OpenAI. Hati-hati saat menentukan kemampuan model, karena dapat mempengaruhi performa Roo Code.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index b97d96a61b..37502d73cf 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -390,6 +390,13 @@ "learnMore": "Scopri di più sul routing dei fornitori" } }, + "qwenCode": { + "oauthPath": "Percorso credenziali OAuth (opzionale)", + "oauthPathDescription": "Percorso del file delle credenziali OAuth. Lasciare vuoto per utilizzare la posizione predefinita (~/.qwen/oauth_creds.json).", + "description": "Questo provider utilizza l'autenticazione OAuth dal servizio Qwen e non richiede chiavi API.", + "instructions": "Si prega di seguire la documentazione ufficiale per ottenere il file di autorizzazione e posizionarlo nel percorso specificato.", + "setupLink": "Documentazione ufficiale Qwen" + }, "customModel": { "capabilities": "Configura le capacità e i prezzi del tuo modello personalizzato compatibile con OpenAI. Fai attenzione quando specifichi le capacità del modello, poiché possono influenzare le prestazioni di Roo Code.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 8061418d38..195ed9c422 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -390,6 +390,13 @@ "learnMore": "プロバイダールーティングについて詳しく知る" } }, + "qwenCode": { + "oauthPath": "OAuth認証情報パス(オプション)", + "oauthPathDescription": "OAuth認証情報ファイルへのパス。デフォルトの場所(~/.qwen/oauth_creds.json)を使用する場合は空のままにしてください。", + "description": "このプロバイダーはQwenサービスからのOAuth認証を使用し、APIキーは必要ありません。", + "instructions": "公式ドキュメントに従って認証ファイルを取得し、指定されたパスに配置してください。", + "setupLink": "Qwen公式ドキュメント" + }, "customModel": { "capabilities": "カスタムOpenAI互換モデルの機能と価格を設定します。モデルの機能はRoo Codeのパフォーマンスに影響を与える可能性があるため、慎重に指定してください。", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 08f3a8c4e7..b6119d31f3 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -390,6 +390,13 @@ "learnMore": "제공자 라우팅에 대해 자세히 알아보기" } }, + "qwenCode": { + "oauthPath": "OAuth 자격 증명 경로 (선택사항)", + "oauthPathDescription": "OAuth 자격 증명 파일의 경로입니다. 기본 위치(~/.qwen/oauth_creds.json)를 사용하려면 비워두세요.", + "description": "이 공급자는 Qwen 서비스의 OAuth 인증을 사용하며 API 키가 필요하지 않습니다.", + "instructions": "공식 문서를 따라 인증 파일을 얻고 지정된 경로에 배치하세요.", + "setupLink": "Qwen 공식 문서" + }, "customModel": { "capabilities": "사용자 정의 OpenAI 호환 모델의 기능과 가격을 구성하세요. 모델 기능이 Roo Code의 성능에 영향을 미칠 수 있으므로 신중하게 지정하세요.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index cc4eb2c90f..325ed5e4e8 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -390,6 +390,13 @@ "learnMore": "Meer informatie over providerroutering" } }, + "qwenCode": { + "oauthPath": "OAuth-referentiepad (optioneel)", + "oauthPathDescription": "Pad naar het OAuth-referentiebestand. Laat leeg om de standaardlocatie (~/.qwen/oauth_creds.json) te gebruiken.", + "description": "Deze provider gebruikt OAuth-authenticatie van de Qwen-service en vereist geen API-sleutels.", + "instructions": "Volg de officiële documentatie om het autorisatiebestand te verkrijgen en plaats het in het opgegeven pad.", + "setupLink": "Qwen Officiële Documentatie" + }, "customModel": { "capabilities": "Stel de mogelijkheden en prijzen in voor je aangepaste OpenAI-compatibele model. Wees voorzichtig met het opgeven van de modelmogelijkheden, want deze kunnen de prestaties van Roo Code beïnvloeden.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 107be09fdd..0af06a613e 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -390,6 +390,13 @@ "learnMore": "Dowiedz się więcej o routingu dostawców" } }, + "qwenCode": { + "oauthPath": "Ścieżka poświadczeń OAuth (opcjonalna)", + "oauthPathDescription": "Ścieżka do pliku poświadczeń OAuth. Pozostaw puste, aby użyć domyślnej lokalizacji (~/.qwen/oauth_creds.json).", + "description": "Ten dostawca używa uwierzytelniania OAuth z usługi Qwen i nie wymaga kluczy API.", + "instructions": "Proszę postępować zgodnie z oficjalną dokumentacją, aby uzyskać plik autoryzacji i umieścić go w określonej ścieżce.", + "setupLink": "Oficjalna dokumentacja Qwen" + }, "customModel": { "capabilities": "Skonfiguruj możliwości i ceny swojego niestandardowego modelu zgodnego z OpenAI. Zachowaj ostrożność podczas określania możliwości modelu, ponieważ mogą one wpływać na wydajność Roo Code.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 54343a2fc5..151984b34c 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -390,6 +390,13 @@ "learnMore": "Saiba mais sobre roteamento de provedores" } }, + "qwenCode": { + "oauthPath": "Caminho das credenciais OAuth (opcional)", + "oauthPathDescription": "Caminho para o arquivo de credenciais OAuth. Deixe vazio para usar o local padrão (~/.qwen/oauth_creds.json).", + "description": "Este provedor usa autenticação OAuth do serviço Qwen e não requer chaves de API.", + "instructions": "Por favor, siga a documentação oficial para obter o arquivo de autorização e colocá-lo no caminho especificado.", + "setupLink": "Documentação oficial do Qwen" + }, "customModel": { "capabilities": "Configure as capacidades e preços para seu modelo personalizado compatível com OpenAI. Tenha cuidado ao especificar as capacidades do modelo, pois elas podem afetar como o Roo Code funciona.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index ac5fafdc17..29c4cee2aa 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -390,6 +390,13 @@ "learnMore": "Подробнее о маршрутизации провайдеров" } }, + "qwenCode": { + "oauthPath": "Путь к учетным данным OAuth (необязательно)", + "oauthPathDescription": "Путь к файлу учетных данных OAuth. Оставьте пустым для использования местоположения по умолчанию (~/.qwen/oauth_creds.json).", + "description": "Этот провайдер использует OAuth-аутентификацию от сервиса Qwen и не требует API-ключей.", + "instructions": "Пожалуйста, следуйте официальной документации для получения файла авторизации и размещения его по указанному пути.", + "setupLink": "Официальная документация Qwen" + }, "customModel": { "capabilities": "Настройте возможности и стоимость вашей пользовательской модели, совместимой с OpenAI. Будьте осторожны при указании возможностей модели, это может повлиять на работу Roo Code.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 0c4138e783..901c79acfe 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -390,6 +390,13 @@ "learnMore": "Sağlayıcı yönlendirmesi hakkında daha fazla bilgi edinin" } }, + "qwenCode": { + "oauthPath": "OAuth Kimlik Bilgileri Yolu (isteğe bağlı)", + "oauthPathDescription": "OAuth kimlik bilgileri dosyasının yolu. Varsayılan konumu (~/.qwen/oauth_creds.json) kullanmak için boş bırakın.", + "description": "Bu sağlayıcı Qwen hizmetinden OAuth kimlik doğrulaması kullanır ve API anahtarları gerektirmez.", + "instructions": "Lütfen yetkilendirme dosyasını almak ve belirtilen yola yerleştirmek için resmi belgeleri takip edin.", + "setupLink": "Qwen Resmi Belgeleri" + }, "customModel": { "capabilities": "Özel OpenAI uyumlu modelinizin yeteneklerini ve fiyatlandırmasını yapılandırın. Model yeteneklerini belirtirken dikkatli olun, çünkü bunlar Roo Code'un performansını etkileyebilir.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 5b2a5a6ef8..50063c2c1d 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -390,6 +390,13 @@ "learnMore": "Tìm hiểu thêm về định tuyến nhà cung cấp" } }, + "qwenCode": { + "oauthPath": "Đường dẫn thông tin xác thực OAuth (tùy chọn)", + "oauthPathDescription": "Đường dẫn đến tệp thông tin xác thực OAuth. Để trống để sử dụng vị trí mặc định (~/.qwen/oauth_creds.json).", + "description": "Nhà cung cấp này sử dụng xác thực OAuth từ dịch vụ Qwen và không yêu cầu khóa API.", + "instructions": "Vui lòng làm theo tài liệu chính thức để lấy tệp ủy quyền và đặt nó trong đường dẫn được chỉ định.", + "setupLink": "Tài liệu chính thức Qwen" + }, "customModel": { "capabilities": "Cấu hình các khả năng và giá cả cho mô hình tương thích OpenAI tùy chỉnh của bạn. Hãy cẩn thận khi chỉ định khả năng của mô hình, vì chúng có thể ảnh hưởng đến cách Roo Code hoạt động.", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 9e56ba74ee..b5204a1201 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -390,6 +390,13 @@ "learnMore": "了解更多" } }, + "qwenCode": { + "oauthPath": "OAuth 凭据路径(可选)", + "oauthPathDescription": "OAuth 凭据文件的路径。留空以使用默认位置 (~/.qwen/oauth_creds.json)。", + "description": "此提供商使用来自 Qwen 服务的 OAuth 身份验证,不需要 API 密钥。", + "instructions": "请按照官方文档获取授权文件并将其放置在指定路径中。", + "setupLink": "Qwen 官方文档" + }, "customModel": { "capabilities": "自定义模型配置注意事项:\n• 确保兼容OpenAI接口规范\n• 错误配置可能导致功能异常\n• 价格参数影响费用统计", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 6bfab1435c..e264d68b30 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -390,6 +390,13 @@ "learnMore": "了解更多關於供應商路由的資訊" } }, + "qwenCode": { + "oauthPath": "OAuth 憑證路徑(可選)", + "oauthPathDescription": "OAuth 憑證檔案的路徑。留空以使用預設位置 (~/.qwen/oauth_creds.json)。", + "description": "此供應商使用來自 Qwen 服務的 OAuth 身份驗證,不需要 API 金鑰。", + "instructions": "請按照官方文件取得授權檔案並將其放置在指定路徑中。", + "setupLink": "Qwen 官方文件" + }, "customModel": { "capabilities": "設定自訂 OpenAI 相容模型的功能和定價。請謹慎設定模型功能,因為這會影響 Roo Code 的運作方式。", "maxTokens": { diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 348a373059..84b92232b3 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -116,6 +116,9 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri return i18next.t("settings:validation.apiKey") } break + case "qwen-code": + // OAuth-based provider, no API key validation needed + break case "fireworks": if (!apiConfiguration.fireworksApiKey) { return i18next.t("settings:validation.apiKey")