diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 3fa7094d87..105ccd9c29 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -50,6 +50,7 @@ export const providerNames = [ "doubao", "unbound", "requesty", + "qwen-code", "human-relay", "fake-ai", "xai", @@ -311,6 +312,10 @@ const ioIntelligenceSchema = apiModelIdProviderModelSchema.extend({ ioIntelligenceApiKey: z.string().optional(), }) +const qwenCodeSchema = apiModelIdProviderModelSchema.extend({ + qwenCodeOAuthPath: z.string().optional(), +}) + const rooSchema = apiModelIdProviderModelSchema.extend({ // No additional fields needed - uses cloud authentication }) @@ -352,6 +357,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })), 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") })), rooSchema.merge(z.object({ apiProvider: z.literal("roo") })), defaultSchema, ]) @@ -390,6 +396,7 @@ export const providerSettingsSchema = z.object({ ...fireworksSchema.shape, ...featherlessSchema.shape, ...ioIntelligenceSchema.shape, + ...qwenCodeSchema.shape, ...rooSchema.shape, ...codebaseIndexProviderSchema.shape, }) @@ -440,7 +447,7 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str } export const MODELS_BY_PROVIDER: Record< - Exclude, + Exclude, { id: ProviderName; label: string; models: string[] } > = { anthropic: { diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 8ca9c2c9b2..27951f0f1a 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -19,6 +19,7 @@ export * from "./moonshot.js" export * from "./ollama.js" export * from "./openai.js" export * from "./openrouter.js" +export * from "./qwen-code.js" export * from "./requesty.js" export * from "./roo.js" export * from "./sambanova.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 6c70a1485d..ba08b87895 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -15,6 +15,7 @@ import { OpenAiHandler, LmStudioHandler, GeminiHandler, + QwenCodeHandler, OpenAiNativeHandler, DeepSeekHandler, MoonshotHandler, @@ -148,6 +149,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new RooHandler(options) case "featherless": return new FeatherlessHandler(options) + case "qwen-code": + return new QwenCodeHandler(options) default: apiProvider satisfies "gemini-cli" | undefined return new AnthropicHandler(options) diff --git a/src/api/providers/__tests__/qwen-code.spec.ts b/src/api/providers/__tests__/qwen-code.spec.ts new file mode 100644 index 0000000000..9edb75cd73 --- /dev/null +++ b/src/api/providers/__tests__/qwen-code.spec.ts @@ -0,0 +1,54 @@ +import { describe, it, expect, vi } from "vitest" +import { QwenCodeHandler } from "../qwen-code" +import { ApiHandlerOptions } from "../../../shared/api" + +// Mock fs +vi.mock("fs", () => ({ + existsSync: vi.fn(), + readFileSync: vi.fn(), +})) + +// Mock os +vi.mock("os", () => ({ + homedir: () => "/home/user", +})) + +// Mock path +vi.mock("path", () => ({ + resolve: vi.fn((...args) => args.join("/")), + join: vi.fn((...args) => args.join("/")), +})) + +describe("QwenCodeHandler", () => { + it("should initialize with correct model configuration", () => { + const options: ApiHandlerOptions = { + apiModelId: "qwen3-coder-plus", + } + const handler = new QwenCodeHandler(options) + + const model = handler.getModel() + expect(model.id).toBe("qwen3-coder-plus") + expect(model.info).toBeDefined() + expect(model.info?.supportsPromptCache).toBe(false) + }) + + it("should use default model when none specified", () => { + const options: ApiHandlerOptions = {} + const handler = new QwenCodeHandler(options) + + const model = handler.getModel() + expect(model.id).toBe("qwen3-coder-plus") // default model + expect(model.info).toBeDefined() + }) + + it("should use custom oauth path when provided", () => { + const customPath = "/custom/path/oauth.json" + const options: ApiHandlerOptions = { + qwenCodeOAuthPath: customPath, + } + const handler = new QwenCodeHandler(options) + + // Handler should initialize without throwing + expect(handler).toBeDefined() + }) +}) diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index d256fbbe55..e2b9047dfc 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -21,6 +21,7 @@ export { OllamaHandler } from "./ollama" export { OpenAiNativeHandler } from "./openai-native" export { OpenAiHandler } from "./openai" export { OpenRouterHandler } from "./openrouter" +export { QwenCodeHandler } from "./qwen-code" export { RequestyHandler } from "./requesty" export { SambaNovaHandler } from "./sambanova" export { UnboundHandler } from "./unbound" diff --git a/src/api/providers/qwen-code.ts b/src/api/providers/qwen-code.ts new file mode 100644 index 0000000000..b8a8cdacf0 --- /dev/null +++ b/src/api/providers/qwen-code.ts @@ -0,0 +1,289 @@ +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(): string { + 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() + const credsStr = await fs.readFile(keyFile, "utf-8") + return JSON.parse(credsStr) + } catch (error) { + console.error(`Error reading or parsing credentials file at ${getQwenCachedCredentialPath()}`) + 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() + 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 74b265f513..a66e056f78 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": "Error en carregar les credencials OAuth. Si us plau, autentica't primer: {{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 856e4e1dce..c2893c9cc8 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 OAuth-Anmeldedaten. Bitte authentifiziere dich zuerst: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index e413bc0890..2878feb790 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 OAuth credentials. Please authenticate first: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 7b2b9a4347..329809ff03 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. Por favor, autentícate primero: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index e9282a0b97..2a83bfa84a 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. Veuillez d'abord vous authentifier : {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 3f5ab60413..b9a409760f 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": "OAuth क्रेडेंशियल्स लोड करने में विफल। कृपया पहले प्रमाणीकरण करें: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 3c43056503..f1805ad499 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. Silakan autentikasi terlebih dahulu: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index c19114baf1..d92f5b8c23 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. Autenticati prima: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index d595484fa1..f01fa0df15 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": "OAuth認証情報の読み込みに失敗しました。最初に認証してください: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 3209952c6d..9a3ea18d80 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": "OAuth 자격 증명을 로드하지 못했습니다. 먼저 인증하세요: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index c0c6ba35e9..4a975d0624 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": "Fout bij laden van OAuth-inloggegevens. Authenticeer eerst: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 475ba069ee..ac7482246e 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ć poświadczeń OAuth. Najpierw się uwierzytelnij: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 55a41fcf1b..5ed3c2ea0f 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. Por favor, autentique primeiro: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 505998daa2..ebf95c5a3c 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. Сначала пройдите аутентификацию: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 9b8af8d94c..a466e876f4 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": "OAuth kimlik bilgileri yüklenemedi. Lütfen önce kimlik doğrulaması yapın: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 4877f297ad..58c708a2de 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. Vui lòng xác thực trước: {{error}}" } }, "warnings": { diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 5bac0d2847..518e78b2be 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": "加载 OAuth 凭据失败。请先进行身份验证:{{error}}" } }, "warnings": { diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 0f82f48d13..0870e39a3e 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": "載入 OAuth 憑證失敗。請先進行驗證:{{error}}" + }, "mode_import_failed": "匯入模式失敗:{{error}}" }, "warnings": { diff --git a/src/shared/checkExistApiConfig.ts b/src/shared/checkExistApiConfig.ts index 8acb88ed3f..319beb2b5e 100644 --- a/src/shared/checkExistApiConfig.ts +++ b/src/shared/checkExistApiConfig.ts @@ -5,8 +5,11 @@ export function checkExistKey(config: ProviderSettings | undefined) { return false } - // 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)) { + // Special case for human-relay, fake-ai, claude-code, roo, and qwen-code providers which don't need any configuration. + 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 13a8ab4848..98fc2c3847 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -33,6 +33,7 @@ import { fireworksDefaultModelId, featherlessDefaultModelId, ioIntelligenceDefaultModelId, + qwenCodeDefaultModelId, rooDefaultModelId, } from "@roo-code/types" @@ -80,6 +81,7 @@ import { OpenAI, OpenAICompatible, OpenRouter, + QwenCode, Requesty, SambaNova, Unbound, @@ -331,6 +333,7 @@ const ApiOptions = ({ fireworks: { field: "apiModelId", default: fireworksDefaultModelId }, featherless: { field: "apiModelId", default: featherlessDefaultModelId }, "io-intelligence": { field: "ioIntelligenceModelId", default: ioIntelligenceDefaultModelId }, + "qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId }, roo: { field: "apiModelId", default: rooDefaultModelId }, openai: { field: "openAiModelId" }, ollama: { field: "ollamaModelId" }, @@ -589,6 +592,10 @@ const ApiOptions = ({ )} + {selectedProvider === "qwen-code" && ( + + )} + {selectedProvider === "roo" && (
{cloudIsAuthenticated ? ( diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index cdeb71814d..bc44df83cf 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -10,6 +10,7 @@ import { geminiModels, mistralModels, openAiNativeModels, + qwenCodeModels, vertexModels, xaiModels, groqModels, @@ -33,6 +34,7 @@ export const MODELS_BY_PROVIDER: Partial a.label.localeCompare(b.label)) diff --git a/webview-ui/src/components/settings/providers/QwenCode.tsx b/webview-ui/src/components/settings/providers/QwenCode.tsx new file mode 100644 index 0000000000..4689b5fa9c --- /dev/null +++ b/webview-ui/src/components/settings/providers/QwenCode.tsx @@ -0,0 +1,58 @@ +import { useCallback } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +import type { ProviderSettings } from "@roo-code/types" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" + +import { inputEventTransform } from "../transforms" + +type QwenCodeProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => 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..46fea622c9 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -17,6 +17,7 @@ export { Ollama } from "./Ollama" export { OpenAI } from "./OpenAI" export { OpenAICompatible } from "./OpenAICompatible" export { OpenRouter } from "./OpenRouter" +export { QwenCode } from "./QwenCode" export { Requesty } from "./Requesty" export { SambaNova } from "./SambaNova" export { Unbound } from "./Unbound" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 75a4a968ad..3d71b3ae23 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -50,6 +50,8 @@ import { featherlessDefaultModelId, ioIntelligenceDefaultModelId, ioIntelligenceModels, + qwenCodeDefaultModelId, + qwenCodeModels, rooDefaultModelId, rooModels, BEDROCK_CLAUDE_SONNET_4_MODEL_ID, @@ -305,6 +307,11 @@ function getSelectedModel({ routerModels["io-intelligence"]?.[id] ?? ioIntelligenceModels[id as keyof typeof ioIntelligenceModels] return { id, info } } + case "qwen-code": { + const id = apiConfiguration.apiModelId ?? qwenCodeDefaultModelId + const info = qwenCodeModels[id as keyof typeof qwenCodeModels] + return { id, info } + } case "roo": { const id = apiConfiguration.apiModelId ?? rooDefaultModelId const info = rooModels[id as keyof typeof rooModels] @@ -314,7 +321,7 @@ function getSelectedModel({ // case "human-relay": // case "fake-ai": default: { - provider satisfies "anthropic" | "gemini-cli" | "human-relay" | "fake-ai" + provider satisfies "anthropic" | "gemini-cli" | "human-relay" | "fake-ai" | "qwen-code" const id = apiConfiguration.apiModelId ?? anthropicDefaultModelId const baseInfo = anthropicModels[id as keyof typeof anthropicModels] diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 54d66c17c7..42d33536b8 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -463,6 +463,13 @@ "placeholder": "Per defecte: claude", "maxTokensLabel": "Tokens màxims de sortida", "maxTokensDescription": "Nombre màxim de tokens de sortida per a les respostes de Claude Code. El valor per defecte és 8000." + }, + "qwenCode": { + "oauthPath": "Ruta de credencials OAuth (opcional)", + "oauthPathDescription": "Ruta del fitxer de credencials OAuth. Deixa-ho buit per utilitzar la ubicació per defecte (~/.qwen/oauth_creds.json).", + "description": "Aquest proveïdor utilitza autenticació OAuth del servei Qwen i no requereix claus API.", + "instructions": "Si us plau, seguiu la documentació oficial per obtenir el fitxer d'autorització i col·locar-lo en la ruta especificada.", + "setupLink": "Documentació oficial de Qwen" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 402e57c8dc..bb305fb85c 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -463,6 +463,13 @@ "placeholder": "Standard: claude", "maxTokensLabel": "Maximale Ausgabe-Tokens", "maxTokensDescription": "Maximale Anzahl an Ausgabe-Tokens für Claude Code-Antworten. Standard ist 8000." + }, + "qwenCode": { + "oauthPath": "OAuth-Anmeldedaten Pfad (optional)", + "oauthPathDescription": "Pfad zur OAuth-Anmeldedatei. Leer lassen, um den Standardort zu verwenden (~/.qwen/oauth_creds.json).", + "description": "Dieser Anbieter verwendet OAuth-Authentifizierung vom Qwen-Service und benötigt keine API-Schlüssel.", + "instructions": "Bitte folge der offiziellen Dokumentation, um die Autorisierungsdatei zu erhalten und sie im angegebenen Pfad zu platzieren.", + "setupLink": "Qwen Offizielle Dokumentation" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 63a9445924..d789700947 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -462,6 +462,13 @@ "placeholder": "Default: claude", "maxTokensLabel": "Max Output Tokens", "maxTokensDescription": "Maximum number of output tokens for Claude Code responses. Default is 8000." + }, + "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" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index a67e2520d0..81531a06c2 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -463,6 +463,13 @@ "placeholder": "Por defecto: claude", "maxTokensLabel": "Tokens máximos de salida", "maxTokensDescription": "Número máximo de tokens de salida para las respuestas de Claude Code. El valor predeterminado es 8000." + }, + "qwenCode": { + "oauthPath": "Ruta de credenciales OAuth (opcional)", + "oauthPathDescription": "Ruta del archivo de credenciales OAuth. Déjalo 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 API.", + "instructions": "Por favor, sigue la documentación oficial para obtener el archivo de autorización y colocarlo en la ruta especificada.", + "setupLink": "Documentación oficial de Qwen" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 36e4629c43..bef515ddf2 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -463,6 +463,13 @@ "placeholder": "Défaut : claude", "maxTokensLabel": "Jetons de sortie max", "maxTokensDescription": "Nombre maximum de jetons de sortie pour les réponses de Claude Code. La valeur par défaut est 8000." + }, + "qwenCode": { + "oauthPath": "Chemin des identifiants OAuth (optionnel)", + "oauthPathDescription": "Chemin du 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" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index a419e7a688..024ef06b21 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -463,6 +463,13 @@ "placeholder": "डिफ़ॉल्ट: claude", "maxTokensLabel": "अधिकतम आउटपुट टोकन", "maxTokensDescription": "Claude Code प्रतिक्रियाओं के लिए आउटपुट टोकन की अधिकतम संख्या। डिफ़ॉल्ट 8000 है।" + }, + "qwenCode": { + "oauthPath": "OAuth क्रेडेंशियल्स पथ (वैकल्पिक)", + "oauthPathDescription": "OAuth क्रेडेंशियल्स फ़ाइल का पथ। डिफ़ॉल्ट स्थान (~/.qwen/oauth_creds.json) का उपयोग करने के लिए खाली छोड़ें।", + "description": "यह प्रदाता Qwen सेवा से OAuth प्रमाणीकरण का उपयोग करता है और API कुंजियों की आवश्यकता नहीं है।", + "instructions": "कृपया प्राधिकरण फ़ाइल प्राप्त करने और इसे निर्दिष्ट पथ में रखने के लिए आधिकारिक दस्तावेज़ीकरण का पालन करें।", + "setupLink": "Qwen आधिकारिक दस्तावेज़ीकरण" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index fe16c7724a..7eb673601a 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -467,6 +467,13 @@ "placeholder": "Default: claude", "maxTokensLabel": "Token Output Maks", "maxTokensDescription": "Jumlah maksimum token output untuk respons Claude Code. Default adalah 8000." + }, + "qwenCode": { + "oauthPath": "Jalur Kredensial OAuth (opsional)", + "oauthPathDescription": "Jalur ke file kredensial OAuth. Biarkan kosong untuk menggunakan lokasi default (~/.qwen/oauth_creds.json).", + "description": "Provider ini menggunakan autentikasi OAuth dari layanan Qwen dan tidak memerlukan API key.", + "instructions": "Silakan ikuti dokumentasi resmi untuk memperoleh file otorisasi dan menempatkannya di jalur yang ditentukan.", + "setupLink": "Dokumentasi Resmi Qwen" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 89b900de98..816570e98b 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -463,6 +463,13 @@ "placeholder": "Predefinito: claude", "maxTokensLabel": "Token di output massimi", "maxTokensDescription": "Numero massimo di token di output per le risposte di Claude Code. Il valore predefinito è 8000." + }, + "qwenCode": { + "oauthPath": "Percorso credenziali OAuth (opzionale)", + "oauthPathDescription": "Percorso del file delle credenziali OAuth. Lascia 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": "Segui la documentazione ufficiale per ottenere il file di autorizzazione e posizionarlo nel percorso specificato.", + "setupLink": "Documentazione ufficiale Qwen" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index d597f259d2..c437b2c89a 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -463,6 +463,13 @@ "placeholder": "デフォルト:claude", "maxTokensLabel": "最大出力トークン", "maxTokensDescription": "Claude Codeレスポンスの最大出力トークン数。デフォルトは8000です。" + }, + "qwenCode": { + "oauthPath": "OAuth認証情報パス(オプション)", + "oauthPathDescription": "OAuth認証情報ファイルのパス。デフォルトの場所(~/.qwen/oauth_creds.json)を使用する場合は空にしてください。", + "description": "このプロバイダーはQwenサービスからのOAuth認証を使用し、APIキーは必要ありません。", + "instructions": "認証ファイルを取得し、指定されたパスに配置するには、公式ドキュメントに従ってください。", + "setupLink": "Qwen公式ドキュメント" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 381622ad74..9328c08b7f 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -463,6 +463,13 @@ "placeholder": "기본값: claude", "maxTokensLabel": "최대 출력 토큰", "maxTokensDescription": "Claude Code 응답의 최대 출력 토큰 수. 기본값은 8000입니다." + }, + "qwenCode": { + "oauthPath": "OAuth 자격 증명 경로 (선택사항)", + "oauthPathDescription": "OAuth 자격 증명 파일의 경로입니다. 기본 위치(~/.qwen/oauth_creds.json)를 사용하려면 비워두세요.", + "description": "이 공급자는 Qwen 서비스의 OAuth 인증을 사용하며 API 키가 필요하지 않습니다.", + "instructions": "인증 파일을 획득하고 지정된 경로에 배치하려면 공식 문서를 따르세요.", + "setupLink": "Qwen 공식 문서" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 27811c0bca..bb71688141 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -463,6 +463,13 @@ "placeholder": "Standaard: claude", "maxTokensLabel": "Max Output Tokens", "maxTokensDescription": "Maximaal aantal output-tokens voor Claude Code-reacties. Standaard is 8000." + }, + "qwenCode": { + "oauthPath": "OAuth-inloggegevens pad (optioneel)", + "oauthPathDescription": "Pad naar het OAuth-inloggegevens bestand. 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 het in het opgegeven pad te plaatsen.", + "setupLink": "Qwen officiële documentatie" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index b101a98b78..c19c6e263f 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -463,6 +463,13 @@ "placeholder": "Domyślnie: claude", "maxTokensLabel": "Maksymalna liczba tokenów wyjściowych", "maxTokensDescription": "Maksymalna liczba tokenów wyjściowych dla odpowiedzi Claude Code. Domyślnie 8000." + }, + "qwenCode": { + "oauthPath": "Ścieżka poświadczeń OAuth (opcjonalnie)", + "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": "Postępuj zgodnie z oficjalną dokumentacją, aby uzyskać plik autoryzacji i umieścić go w określonej ścieżce.", + "setupLink": "Oficjalna dokumentacja Qwen" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 20064675c8..3896aa82b8 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -463,6 +463,13 @@ "placeholder": "Padrão: claude", "maxTokensLabel": "Tokens de saída máximos", "maxTokensDescription": "Número máximo de tokens de saída para respostas do Claude Code. O padrão é 8000." + }, + "qwenCode": { + "oauthPath": "Caminho das credenciais OAuth (opcional)", + "oauthPathDescription": "Caminho para o arquivo de credenciais OAuth. Deixe vazio para usar a localização 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": "Siga a documentação oficial para obter o arquivo de autorização e colocá-lo no caminho especificado.", + "setupLink": "Documentação oficial do Qwen" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 4143a331fc..68bfcb7f92 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -463,6 +463,13 @@ "placeholder": "По умолчанию: claude", "maxTokensLabel": "Макс. выходных токенов", "maxTokensDescription": "Максимальное количество выходных токенов для ответов Claude Code. По умолчанию 8000." + }, + "qwenCode": { + "oauthPath": "Путь OAuth-данных (необязательно)", + "oauthPathDescription": "Путь к файлу OAuth-данных. Оставьте пустым для использования местоположения по умолчанию (~/.qwen/oauth_creds.json).", + "description": "Этот провайдер использует OAuth-аутентификацию из сервиса Qwen и не требует API-ключей.", + "instructions": "Следуйте официальной документации для получения файла авторизации и размещения его по указанному пути.", + "setupLink": "Официальная документация Qwen" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 5de8363038..f052325b9e 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -463,6 +463,13 @@ "placeholder": "Varsayılan: claude", "maxTokensLabel": "Maksimum Çıktı Token sayısı", "maxTokensDescription": "Claude Code yanıtları için maksimum çıktı token sayısı. Varsayılan 8000'dir." + }, + "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 anahtarı gerektirmez.", + "instructions": "Yetkilendirme dosyasını almak ve belirtilen yola yerleştirmek için resmi belgeleri takip edin.", + "setupLink": "Qwen resmi belgeleri" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 6fce8db6f4..c04e291b66 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -463,6 +463,13 @@ "placeholder": "Mặc định: claude", "maxTokensLabel": "Số token đầu ra tối đa", "maxTokensDescription": "Số lượng token đầu ra tối đa cho các phản hồi của Claude Code. Mặc định là 8000." + }, + "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 theo dõi tài liệu chính thức để lấy tệp ủy quyền và đặt nó trong đường dẫn đã chỉ định.", + "setupLink": "Tài liệu chính thức Qwen" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 948693768c..ad6b5abd0f 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -459,10 +459,17 @@ "setReasoningLevel": "启用推理工作量", "claudeCode": { "pathLabel": "Claude Code 路径", - "description": "您的 Claude Code CLI 的可选路径。如果未设置,则默认为 “claude”。", + "description": "您的 Claude Code CLI 的可选路径。如果未设置,则默认为 'claude'。", "placeholder": "默认:claude", "maxTokensLabel": "最大输出 Token", "maxTokensDescription": "Claude Code 响应的最大输出 Token 数量。默认为 8000。" + }, + "qwenCode": { + "oauthPath": "OAuth 凭据路径(可选)", + "oauthPathDescription": "OAuth 凭据文件的路径。留空以使用默认位置 (~/.qwen/oauth_creds.json)。", + "description": "此提供商使用来自 Qwen 服务的 OAuth 身份验证,不需要 API 密钥。", + "instructions": "请参照官方文档获取授权文件并将其放置在指定路径。", + "setupLink": "Qwen 官方文档" } }, "browser": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 8dd76aee3f..cb3b394d46 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -463,6 +463,13 @@ "placeholder": "預設:claude", "maxTokensLabel": "最大輸出 Token", "maxTokensDescription": "Claude Code 回應的最大輸出 Token 數量。預設為 8000。" + }, + "qwenCode": { + "oauthPath": "OAuth 憑證路徑(選用)", + "oauthPathDescription": "OAuth 憑證檔案的路徑。留空以使用預設位置 (~/.qwen/oauth_creds.json)。", + "description": "此供應商使用來自 Qwen 服務的 OAuth 身分驗證,不需要 API 金鑰。", + "instructions": "請遵循官方說明文件取得授權檔案,並將其放置在指定路徑。", + "setupLink": "Qwen 官方說明文件" } }, "browser": { diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 348a373059..63986a67a8 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -73,6 +73,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 "openai-native": if (!apiConfiguration.openAiNativeApiKey) { return i18next.t("settings:validation.apiKey")