Browse Source

feat: Add Qwen Code CLI API Support with OAuth Authentication (#7380)

Co-authored-by: Roo Code <[email protected]>
Co-authored-by: daniel-lxs <[email protected]>
Co-authored-by: cte <[email protected]>
roomote[bot] 4 months ago
parent
commit
2e99d5bf1b
34 changed files with 503 additions and 48 deletions
  1. 1 1
      packages/types/npm/package.metadata.json
  2. 9 0
      packages/types/src/provider-settings.ts
  3. 1 0
      packages/types/src/providers/index.ts
  4. 30 0
      packages/types/src/providers/qwen-code.ts
  5. 11 25
      pnpm-lock.yaml
  6. 3 0
      src/api/index.ts
  7. 1 0
      src/api/providers/index.ts
  8. 315 0
      src/api/providers/qwen-code.ts
  9. 1 1
      src/package.json
  10. 5 2
      src/shared/checkExistApiConfig.ts
  11. 7 0
      webview-ui/src/components/settings/ApiOptions.tsx
  12. 3 0
      webview-ui/src/components/settings/constants.ts
  13. 66 0
      webview-ui/src/components/settings/providers/QwenCode.tsx
  14. 1 0
      webview-ui/src/components/settings/providers/index.ts
  15. 8 1
      webview-ui/src/components/ui/hooks/useSelectedModel.ts
  16. 2 1
      webview-ui/src/i18n/locales/ca/settings.json
  17. 2 1
      webview-ui/src/i18n/locales/de/settings.json
  18. 2 1
      webview-ui/src/i18n/locales/en/settings.json
  19. 2 1
      webview-ui/src/i18n/locales/es/settings.json
  20. 2 1
      webview-ui/src/i18n/locales/fr/settings.json
  21. 2 1
      webview-ui/src/i18n/locales/hi/settings.json
  22. 2 1
      webview-ui/src/i18n/locales/id/settings.json
  23. 2 1
      webview-ui/src/i18n/locales/it/settings.json
  24. 2 1
      webview-ui/src/i18n/locales/ja/settings.json
  25. 2 1
      webview-ui/src/i18n/locales/ko/settings.json
  26. 2 1
      webview-ui/src/i18n/locales/nl/settings.json
  27. 2 1
      webview-ui/src/i18n/locales/pl/settings.json
  28. 2 1
      webview-ui/src/i18n/locales/pt-BR/settings.json
  29. 2 1
      webview-ui/src/i18n/locales/ru/settings.json
  30. 2 1
      webview-ui/src/i18n/locales/tr/settings.json
  31. 2 1
      webview-ui/src/i18n/locales/vi/settings.json
  32. 2 1
      webview-ui/src/i18n/locales/zh-CN/settings.json
  33. 2 1
      webview-ui/src/i18n/locales/zh-TW/settings.json
  34. 5 0
      webview-ui/src/utils/validate.ts

+ 1 - 1
packages/types/npm/package.metadata.json

@@ -1,6 +1,6 @@
 {
 {
 	"name": "@roo-code/types",
 	"name": "@roo-code/types",
-	"version": "1.59.0",
+	"version": "1.60.0",
 	"description": "TypeScript type definitions for Roo Code.",
 	"description": "TypeScript type definitions for Roo Code.",
 	"publishConfig": {
 	"publishConfig": {
 		"access": "public",
 		"access": "public",

+ 9 - 0
packages/types/src/provider-settings.ts

@@ -18,6 +18,7 @@ import {
 	mistralModels,
 	mistralModels,
 	moonshotModels,
 	moonshotModels,
 	openAiNativeModels,
 	openAiNativeModels,
+	qwenCodeModels,
 	rooModels,
 	rooModels,
 	sambaNovaModels,
 	sambaNovaModels,
 	vertexModels,
 	vertexModels,
@@ -48,6 +49,7 @@ export const providerNames = [
 	"moonshot",
 	"moonshot",
 	"deepseek",
 	"deepseek",
 	"doubao",
 	"doubao",
+	"qwen-code",
 	"unbound",
 	"unbound",
 	"requesty",
 	"requesty",
 	"human-relay",
 	"human-relay",
@@ -311,6 +313,10 @@ const ioIntelligenceSchema = apiModelIdProviderModelSchema.extend({
 	ioIntelligenceApiKey: z.string().optional(),
 	ioIntelligenceApiKey: z.string().optional(),
 })
 })
 
 
+const qwenCodeSchema = apiModelIdProviderModelSchema.extend({
+	qwenCodeOauthPath: z.string().optional(),
+})
+
 const rooSchema = apiModelIdProviderModelSchema.extend({
 const rooSchema = apiModelIdProviderModelSchema.extend({
 	// No additional fields needed - uses cloud authentication
 	// No additional fields needed - uses cloud authentication
 })
 })
@@ -352,6 +358,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
 	fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })),
 	fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })),
 	featherlessSchema.merge(z.object({ apiProvider: z.literal("featherless") })),
 	featherlessSchema.merge(z.object({ apiProvider: z.literal("featherless") })),
 	ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })),
 	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") })),
 	rooSchema.merge(z.object({ apiProvider: z.literal("roo") })),
 	defaultSchema,
 	defaultSchema,
 ])
 ])
@@ -390,6 +397,7 @@ export const providerSettingsSchema = z.object({
 	...fireworksSchema.shape,
 	...fireworksSchema.shape,
 	...featherlessSchema.shape,
 	...featherlessSchema.shape,
 	...ioIntelligenceSchema.shape,
 	...ioIntelligenceSchema.shape,
+	...qwenCodeSchema.shape,
 	...rooSchema.shape,
 	...rooSchema.shape,
 	...codebaseIndexProviderSchema.shape,
 	...codebaseIndexProviderSchema.shape,
 })
 })
@@ -506,6 +514,7 @@ export const MODELS_BY_PROVIDER: Record<
 		label: "OpenAI",
 		label: "OpenAI",
 		models: Object.keys(openAiNativeModels),
 		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) },
 	roo: { id: "roo", label: "Roo", models: Object.keys(rooModels) },
 	sambanova: {
 	sambanova: {
 		id: "sambanova",
 		id: "sambanova",

+ 1 - 0
packages/types/src/providers/index.ts

@@ -19,6 +19,7 @@ export * from "./moonshot.js"
 export * from "./ollama.js"
 export * from "./ollama.js"
 export * from "./openai.js"
 export * from "./openai.js"
 export * from "./openrouter.js"
 export * from "./openrouter.js"
+export * from "./qwen-code.js"
 export * from "./requesty.js"
 export * from "./requesty.js"
 export * from "./roo.js"
 export * from "./roo.js"
 export * from "./sambanova.js"
 export * from "./sambanova.js"

+ 30 - 0
packages/types/src/providers/qwen-code.ts

@@ -0,0 +1,30 @@
+import type { ModelInfo } from "../model.js"
+
+export type QwenCodeModelId = "qwen3-coder-plus" | "qwen3-coder-flash"
+
+export const qwenCodeDefaultModelId: QwenCodeModelId = "qwen3-coder-plus"
+
+export const qwenCodeModels = {
+	"qwen3-coder-plus": {
+		maxTokens: 65_536,
+		contextWindow: 1_000_000,
+		supportsImages: false,
+		supportsPromptCache: false,
+		inputPrice: 0,
+		outputPrice: 0,
+		cacheWritesPrice: 0,
+		cacheReadsPrice: 0,
+		description: "Qwen3 Coder Plus - High-performance coding model with 1M context window for large codebases",
+	},
+	"qwen3-coder-flash": {
+		maxTokens: 65_536,
+		contextWindow: 1_000_000,
+		supportsImages: false,
+		supportsPromptCache: false,
+		inputPrice: 0,
+		outputPrice: 0,
+		cacheWritesPrice: 0,
+		cacheReadsPrice: 0,
+		description: "Qwen3 Coder Flash - Fast coding model with 1M context window optimized for speed",
+	},
+} as const satisfies Record<QwenCodeModelId, ModelInfo>

+ 11 - 25
pnpm-lock.yaml

@@ -584,8 +584,8 @@ importers:
         specifier: ^1.14.0
         specifier: ^1.14.0
         version: 1.14.0([email protected])
         version: 1.14.0([email protected])
       '@roo-code/cloud':
       '@roo-code/cloud':
-        specifier: ^0.19.0
-        version: 0.19.0
+        specifier: ^0.21.0
+        version: 0.21.0
       '@roo-code/ipc':
       '@roo-code/ipc':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../packages/ipc
         version: link:../packages/ipc
@@ -3262,11 +3262,11 @@ packages:
     cpu: [x64]
     cpu: [x64]
     os: [win32]
     os: [win32]
 
 
-  '@roo-code/[email protected]9.0':
-    resolution: {integrity: sha512-alZ3X4+TPqRr0xSs9v/UDo3eTlcHaI8ZW8AbWPDtgqf86P8govnyM2hVUMhGXete3AlbYIPRE/9w3/7MrcIjsA==}
+  '@roo-code/cloud@0.21.0':
+    resolution: {integrity: sha512-yNVybIjaS7Hy8GwDtGJc76N1WpCXGaCSlAEsW7VGjnojpxaIzV2GcJP1j1hg5q8HqLQnU4ixV0qXxOkxwhkEiA==}
 
 
-  '@roo-code/types@1.55.0':
-    resolution: {integrity: sha512-+T5MP8IQcDp7htnGDnk3M4n7S5eYk6jNkw3VBSUBZRhS4EE2GuPDI+CcdmhnDiMb6NMV6yseL+CT4G4QV5ktUw==}
+  '@roo-code/types@1.60.0':
+    resolution: {integrity: sha512-tQO6njPr/ZDNBoSHQg1/dpxfVEYeUzpKcernUxgJzmttn1zJbS0sc3CfUyPYOfYKB331z6O3KFUpaiqYFje1wA==}
 
 
   '@sec-ant/[email protected]':
   '@sec-ant/[email protected]':
     resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
     resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@@ -4072,9 +4072,6 @@ packages:
   '@types/[email protected]':
   '@types/[email protected]':
     resolution: {integrity: sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==}
     resolution: {integrity: sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==}
 
 
-  '@types/[email protected]':
-    resolution: {integrity: sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==}
-
   '@types/[email protected]':
   '@types/[email protected]':
     resolution: {integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==}
     resolution: {integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==}
 
 
@@ -9490,9 +9487,6 @@ packages:
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
     resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
 
 
-  [email protected]:
-    resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
-
   [email protected]:
   [email protected]:
     resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
     resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
 
 
@@ -12569,9 +12563,9 @@ snapshots:
   '@rollup/[email protected]':
   '@rollup/[email protected]':
     optional: true
     optional: true
 
 
-  '@roo-code/[email protected]9.0':
+  '@roo-code/cloud@0.21.0':
     dependencies:
     dependencies:
-      '@roo-code/types': 1.55.0
+      '@roo-code/types': 1.60.0
       ioredis: 5.6.1
       ioredis: 5.6.1
       p-wait-for: 5.0.2
       p-wait-for: 5.0.2
       socket.io-client: 4.8.1
       socket.io-client: 4.8.1
@@ -12581,7 +12575,7 @@ snapshots:
       - supports-color
       - supports-color
       - utf-8-validate
       - utf-8-validate
 
 
-  '@roo-code/types@1.55.0':
+  '@roo-code/types@1.60.0':
     dependencies:
     dependencies:
       zod: 3.25.76
       zod: 3.25.76
 
 
@@ -13574,11 +13568,6 @@ snapshots:
     dependencies:
     dependencies:
       undici-types: 6.19.8
       undici-types: 6.19.8
 
 
-  '@types/[email protected]':
-    dependencies:
-      undici-types: 6.21.0
-    optional: true
-
   '@types/[email protected]':
   '@types/[email protected]':
     dependencies:
     dependencies:
       undici-types: 7.10.0
       undici-types: 7.10.0
@@ -13642,7 +13631,7 @@ snapshots:
 
 
   '@types/[email protected]':
   '@types/[email protected]':
     dependencies:
     dependencies:
-      '@types/node': 20.19.11
+      '@types/node': 24.2.1
     optional: true
     optional: true
 
 
   '@types/[email protected]': {}
   '@types/[email protected]': {}
@@ -13815,7 +13804,7 @@ snapshots:
       sirv: 3.0.1
       sirv: 3.0.1
       tinyglobby: 0.2.14
       tinyglobby: 0.2.14
       tinyrainbow: 2.0.0
       tinyrainbow: 2.0.0
-      vitest: 3.2.4(@types/[email protected])(@types/node@20.17.50)(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
+      vitest: 3.2.4(@types/[email protected])(@types/node@24.2.1)(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
 
 
   '@vitest/[email protected]':
   '@vitest/[email protected]':
     dependencies:
     dependencies:
@@ -19934,9 +19923,6 @@ snapshots:
 
 
   [email protected]: {}
   [email protected]: {}
 
 
-  [email protected]:
-    optional: true
-
   [email protected]: {}
   [email protected]: {}
 
 
   [email protected]: {}
   [email protected]: {}

+ 3 - 0
src/api/index.ts

@@ -30,6 +30,7 @@ import {
 	ChutesHandler,
 	ChutesHandler,
 	LiteLLMHandler,
 	LiteLLMHandler,
 	ClaudeCodeHandler,
 	ClaudeCodeHandler,
+	QwenCodeHandler,
 	SambaNovaHandler,
 	SambaNovaHandler,
 	IOIntelligenceHandler,
 	IOIntelligenceHandler,
 	DoubaoHandler,
 	DoubaoHandler,
@@ -108,6 +109,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 			return new DeepSeekHandler(options)
 			return new DeepSeekHandler(options)
 		case "doubao":
 		case "doubao":
 			return new DoubaoHandler(options)
 			return new DoubaoHandler(options)
+		case "qwen-code":
+			return new QwenCodeHandler(options)
 		case "moonshot":
 		case "moonshot":
 			return new MoonshotHandler(options)
 			return new MoonshotHandler(options)
 		case "vscode-lm":
 		case "vscode-lm":

+ 1 - 0
src/api/providers/index.ts

@@ -21,6 +21,7 @@ export { OllamaHandler } from "./ollama"
 export { OpenAiNativeHandler } from "./openai-native"
 export { OpenAiNativeHandler } from "./openai-native"
 export { OpenAiHandler } from "./openai"
 export { OpenAiHandler } from "./openai"
 export { OpenRouterHandler } from "./openrouter"
 export { OpenRouterHandler } from "./openrouter"
+export { QwenCodeHandler } from "./qwen-code"
 export { RequestyHandler } from "./requesty"
 export { RequestyHandler } from "./requesty"
 export { SambaNovaHandler } from "./sambanova"
 export { SambaNovaHandler } from "./sambanova"
 export { UnboundHandler } from "./unbound"
 export { UnboundHandler } from "./unbound"

+ 315 - 0
src/api/providers/qwen-code.ts

@@ -0,0 +1,315 @@
+import { promises as fs } from "node:fs"
+import { Anthropic } from "@anthropic-ai/sdk"
+import OpenAI from "openai"
+import * as os from "os"
+import * as path from "path"
+
+import type { ModelInfo } from "@roo-code/types"
+import type { ApiHandlerOptions } from "../../shared/api"
+
+import { convertToOpenAiMessages } from "../transform/openai-format"
+import { ApiStream } from "../transform/stream"
+import { BaseProvider } from "./base-provider"
+import type { SingleCompletionHandler } from "../index"
+
+// --- Constants for Qwen OAuth2 ---
+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
+}
+
+interface QwenCodeHandlerOptions extends ApiHandlerOptions {
+	qwenCodeOauthPath?: string
+}
+
+function getQwenCachedCredentialPath(customPath?: string): string {
+	if (customPath) {
+		// Support custom path that starts with ~/ or is absolute
+		if (customPath.startsWith("~/")) {
+			return path.join(os.homedir(), customPath.slice(2))
+		}
+		return path.resolve(customPath)
+	}
+	return path.join(os.homedir(), QWEN_DIR, QWEN_CREDENTIAL_FILENAME)
+}
+
+function objectToUrlEncoded(data: Record<string, string>): string {
+	return Object.keys(data)
+		.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
+		.join("&")
+}
+
+export class QwenCodeHandler extends BaseProvider implements SingleCompletionHandler {
+	protected options: QwenCodeHandlerOptions
+	private credentials: QwenOAuthCredentials | null = null
+	private client: OpenAI | undefined
+	private refreshPromise: Promise<QwenOAuthCredentials> | null = null
+
+	constructor(options: QwenCodeHandlerOptions) {
+		super()
+		this.options = options
+	}
+
+	private ensureClient(): OpenAI {
+		if (!this.client) {
+			// Create the client instance with dummy key initially
+			// 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",
+			})
+		}
+		return this.client
+	}
+
+	private async loadCachedQwenCredentials(): Promise<QwenOAuthCredentials> {
+		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(`Failed to load Qwen OAuth credentials: ${error}`)
+		}
+	}
+
+	private async refreshAccessToken(credentials: QwenOAuthCredentials): Promise<QwenOAuthCredentials> {
+		// If a refresh is already in progress, return the existing promise
+		if (this.refreshPromise) {
+			return this.refreshPromise
+		}
+
+		// Create a new refresh promise
+		this.refreshPromise = this.doRefreshAccessToken(credentials)
+
+		try {
+			const result = await this.refreshPromise
+			return result
+		} finally {
+			// Clear the promise after completion (success or failure)
+			this.refreshPromise = null
+		}
+	}
+
+	private async doRefreshAccessToken(credentials: QwenOAuthCredentials): Promise<QwenOAuthCredentials> {
+		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)
+		try {
+			await fs.writeFile(filePath, JSON.stringify(newCredentials, null, 2))
+		} catch (error) {
+			console.error("Failed to save refreshed credentials:", error)
+			// Continue with the refreshed token in memory even if file write fails
+		}
+
+		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<void> {
+		if (!this.credentials) {
+			this.credentials = await this.loadCachedQwenCredentials()
+		}
+
+		if (!this.isTokenValid(this.credentials)) {
+			this.credentials = await this.refreshAccessToken(this.credentials)
+		}
+
+		// After authentication, update the apiKey and baseURL on the existing client
+		const client = this.ensureClient()
+		client.apiKey = this.credentials.access_token
+		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<T>(apiCall: () => Promise<T>): Promise<T> {
+		try {
+			return await apiCall()
+		} catch (error: any) {
+			if (error.status === 401) {
+				// Token expired, refresh and retry
+				this.credentials = await this.refreshAccessToken(this.credentials!)
+				const client = this.ensureClient()
+				client.apiKey = this.credentials.access_token
+				client.baseURL = this.getBaseUrl(this.credentials)
+				return await apiCall()
+			} else {
+				throw error
+			}
+		}
+	}
+
+	override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+		await this.ensureAuthenticated()
+		const client = this.ensureClient()
+		const model = this.getModel()
+
+		const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
+			role: "system",
+			content: systemPrompt,
+		}
+
+		const convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)]
+
+		const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
+			model: model.id,
+			temperature: 0,
+			messages: convertedMessages,
+			stream: true,
+			stream_options: { include_usage: true },
+			max_completion_tokens: model.info.maxTokens,
+		}
+
+		const stream = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
+
+		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) {
+					// Check for thinking blocks
+					if (newText.includes("<think>") || newText.includes("</think>")) {
+						// Simple parsing for thinking blocks
+						const parts = newText.split(/<\/?think>/g)
+						for (let i = 0; i < parts.length; i++) {
+							if (parts[i]) {
+								if (i % 2 === 0) {
+									// Outside thinking block
+									yield {
+										type: "text",
+										text: parts[i],
+									}
+								} else {
+									// Inside thinking block
+									yield {
+										type: "reasoning",
+										text: parts[i],
+									}
+								}
+							}
+						}
+					} else {
+						yield {
+							type: "text",
+							text: newText,
+						}
+					}
+				}
+			}
+
+			// Handle reasoning content (o1-style)
+			if ("reasoning_content" in delta && delta.reasoning_content) {
+				yield {
+					type: "reasoning",
+					text: (delta.reasoning_content as string | undefined) || "",
+				}
+			}
+
+			if (apiChunk.usage) {
+				yield {
+					type: "usage",
+					inputTokens: apiChunk.usage.prompt_tokens || 0,
+					outputTokens: apiChunk.usage.completion_tokens || 0,
+				}
+			}
+		}
+	}
+
+	override getModel(): { id: string; info: ModelInfo } {
+		const modelId = this.options.apiModelId
+		const { qwenCodeModels, qwenCodeDefaultModelId } = require("@roo-code/types")
+		if (modelId && modelId in qwenCodeModels) {
+			const id = modelId
+			return { id, info: qwenCodeModels[id] }
+		}
+		return {
+			id: qwenCodeDefaultModelId,
+			info: qwenCodeModels[qwenCodeDefaultModelId],
+		}
+	}
+
+	async completePrompt(prompt: string): Promise<string> {
+		await this.ensureAuthenticated()
+		const client = this.ensureClient()
+		const model = this.getModel()
+
+		const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
+			model: model.id,
+			messages: [{ role: "user", content: prompt }],
+			max_completion_tokens: model.info.maxTokens,
+		}
+
+		const response = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
+
+		return response.choices[0]?.message.content || ""
+	}
+}

+ 1 - 1
src/package.json

@@ -432,7 +432,7 @@
 		"@mistralai/mistralai": "^1.9.18",
 		"@mistralai/mistralai": "^1.9.18",
 		"@modelcontextprotocol/sdk": "^1.9.0",
 		"@modelcontextprotocol/sdk": "^1.9.0",
 		"@qdrant/js-client-rest": "^1.14.0",
 		"@qdrant/js-client-rest": "^1.14.0",
-		"@roo-code/cloud": "^0.19.0",
+		"@roo-code/cloud": "^0.21.0",
 		"@roo-code/ipc": "workspace:^",
 		"@roo-code/ipc": "workspace:^",
 		"@roo-code/telemetry": "workspace:^",
 		"@roo-code/telemetry": "workspace:^",
 		"@roo-code/types": "workspace:^",
 		"@roo-code/types": "workspace:^",

+ 5 - 2
src/shared/checkExistApiConfig.ts

@@ -5,8 +5,11 @@ export function checkExistKey(config: ProviderSettings | undefined) {
 		return false
 		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, qwen-code, and roo providers which don't need any configuration.
+	if (
+		config.apiProvider &&
+		["human-relay", "fake-ai", "claude-code", "qwen-code", "roo"].includes(config.apiProvider)
+	) {
 		return true
 		return true
 	}
 	}
 
 

+ 7 - 0
webview-ui/src/components/settings/ApiOptions.tsx

@@ -17,6 +17,7 @@ import {
 	anthropicDefaultModelId,
 	anthropicDefaultModelId,
 	doubaoDefaultModelId,
 	doubaoDefaultModelId,
 	claudeCodeDefaultModelId,
 	claudeCodeDefaultModelId,
+	qwenCodeDefaultModelId,
 	geminiDefaultModelId,
 	geminiDefaultModelId,
 	deepSeekDefaultModelId,
 	deepSeekDefaultModelId,
 	moonshotDefaultModelId,
 	moonshotDefaultModelId,
@@ -80,6 +81,7 @@ import {
 	OpenAI,
 	OpenAI,
 	OpenAICompatible,
 	OpenAICompatible,
 	OpenRouter,
 	OpenRouter,
+	QwenCode,
 	Requesty,
 	Requesty,
 	SambaNova,
 	SambaNova,
 	Unbound,
 	Unbound,
@@ -309,6 +311,7 @@ const ApiOptions = ({
 				anthropic: { field: "apiModelId", default: anthropicDefaultModelId },
 				anthropic: { field: "apiModelId", default: anthropicDefaultModelId },
 				cerebras: { field: "apiModelId", default: cerebrasDefaultModelId },
 				cerebras: { field: "apiModelId", default: cerebrasDefaultModelId },
 				"claude-code": { field: "apiModelId", default: claudeCodeDefaultModelId },
 				"claude-code": { field: "apiModelId", default: claudeCodeDefaultModelId },
+				"qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId },
 				"openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId },
 				"openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId },
 				gemini: { field: "apiModelId", default: geminiDefaultModelId },
 				gemini: { field: "apiModelId", default: geminiDefaultModelId },
 				deepseek: { field: "apiModelId", default: deepSeekDefaultModelId },
 				deepseek: { field: "apiModelId", default: deepSeekDefaultModelId },
@@ -516,6 +519,10 @@ const ApiOptions = ({
 				<Doubao apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 				<Doubao apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 			)}
 			)}
 
 
+			{selectedProvider === "qwen-code" && (
+				<QwenCode apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
+			)}
+
 			{selectedProvider === "moonshot" && (
 			{selectedProvider === "moonshot" && (
 				<Moonshot apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 				<Moonshot apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
 			)}
 			)}

+ 3 - 0
webview-ui/src/components/settings/constants.ts

@@ -10,6 +10,7 @@ import {
 	geminiModels,
 	geminiModels,
 	mistralModels,
 	mistralModels,
 	openAiNativeModels,
 	openAiNativeModels,
+	qwenCodeModels,
 	vertexModels,
 	vertexModels,
 	xaiModels,
 	xaiModels,
 	groqModels,
 	groqModels,
@@ -33,6 +34,7 @@ export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, Mod
 	gemini: geminiModels,
 	gemini: geminiModels,
 	mistral: mistralModels,
 	mistral: mistralModels,
 	"openai-native": openAiNativeModels,
 	"openai-native": openAiNativeModels,
+	"qwen-code": qwenCodeModels,
 	vertex: vertexModels,
 	vertex: vertexModels,
 	xai: xaiModels,
 	xai: xaiModels,
 	groq: groqModels,
 	groq: groqModels,
@@ -55,6 +57,7 @@ export const PROVIDERS = [
 	{ value: "moonshot", label: "Moonshot" },
 	{ value: "moonshot", label: "Moonshot" },
 	{ value: "openai-native", label: "OpenAI" },
 	{ value: "openai-native", label: "OpenAI" },
 	{ value: "openai", label: "OpenAI Compatible" },
 	{ value: "openai", label: "OpenAI Compatible" },
+	{ value: "qwen-code", label: "Qwen Code" },
 	{ value: "vertex", label: "GCP Vertex AI" },
 	{ value: "vertex", label: "GCP Vertex AI" },
 	{ value: "bedrock", label: "Amazon Bedrock" },
 	{ value: "bedrock", label: "Amazon Bedrock" },
 	{ value: "glama", label: "Glama" },
 	{ value: "glama", label: "Glama" },

+ 66 - 0
webview-ui/src/components/settings/providers/QwenCode.tsx

@@ -0,0 +1,66 @@
+import React from "react"
+import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
+import { type ProviderSettings } from "@roo-code/types"
+
+interface QwenCodeProps {
+	apiConfiguration: ProviderSettings
+	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
+}
+
+export const QwenCode: React.FC<QwenCodeProps> = ({ apiConfiguration, setApiConfigurationField }) => {
+	const defaultPath = "~/.qwen/oauth_creds.json"
+
+	const handleInputChange = (e: Event | React.FormEvent<HTMLElement>) => {
+		const element = e.target as HTMLInputElement
+		setApiConfigurationField("qwenCodeOauthPath", element.value)
+	}
+
+	const handleBlur = (e: Event | React.FormEvent<HTMLElement>) => {
+		const element = e.target as HTMLInputElement
+		// If the field is empty on blur, set it to the default value
+		if (!element.value || element.value.trim() === "") {
+			setApiConfigurationField("qwenCodeOauthPath", defaultPath)
+		}
+	}
+
+	return (
+		<div className="flex flex-col gap-4">
+			<div>
+				<VSCodeTextField
+					value={apiConfiguration?.qwenCodeOauthPath || ""}
+					className="w-full mt-1"
+					type="text"
+					onInput={handleInputChange}
+					onBlur={handleBlur}
+					placeholder={defaultPath}>
+					OAuth Credentials Path
+				</VSCodeTextField>
+
+				<p className="text-xs mt-1 text-vscode-descriptionForeground">
+					Path to your Qwen OAuth credentials file. Defaults to ~/.qwen/oauth_creds.json if left empty.
+				</p>
+
+				<div className="text-xs text-vscode-descriptionForeground mt-3">
+					Qwen Code is an OAuth-based API that requires authentication through the official Qwen client.
+					You&apos;ll need to set up OAuth credentials first.
+				</div>
+
+				<div className="text-xs text-vscode-descriptionForeground mt-2">
+					To get started:
+					<br />
+					1. Install the official Qwen client
+					<br />
+					2. Authenticate using your account
+					<br />
+					3. OAuth credentials will be stored automatically
+				</div>
+
+				<VSCodeLink
+					href="https://github.com/QwenLM/qwen-code/blob/main/README.md"
+					className="text-vscode-textLink-foreground mt-2 inline-block text-xs">
+					Setup Instructions
+				</VSCodeLink>
+			</div>
+		</div>
+	)
+}

+ 1 - 0
webview-ui/src/components/settings/providers/index.ts

@@ -17,6 +17,7 @@ export { Ollama } from "./Ollama"
 export { OpenAI } from "./OpenAI"
 export { OpenAI } from "./OpenAI"
 export { OpenAICompatible } from "./OpenAICompatible"
 export { OpenAICompatible } from "./OpenAICompatible"
 export { OpenRouter } from "./OpenRouter"
 export { OpenRouter } from "./OpenRouter"
+export { QwenCode } from "./QwenCode"
 export { Requesty } from "./Requesty"
 export { Requesty } from "./Requesty"
 export { SambaNova } from "./SambaNova"
 export { SambaNova } from "./SambaNova"
 export { Unbound } from "./Unbound"
 export { Unbound } from "./Unbound"

+ 8 - 1
webview-ui/src/components/ui/hooks/useSelectedModel.ts

@@ -52,6 +52,8 @@ import {
 	ioIntelligenceModels,
 	ioIntelligenceModels,
 	rooDefaultModelId,
 	rooDefaultModelId,
 	rooModels,
 	rooModels,
+	qwenCodeDefaultModelId,
+	qwenCodeModels,
 	BEDROCK_CLAUDE_SONNET_4_MODEL_ID,
 	BEDROCK_CLAUDE_SONNET_4_MODEL_ID,
 } from "@roo-code/types"
 } from "@roo-code/types"
 
 
@@ -310,11 +312,16 @@ function getSelectedModel({
 			const info = rooModels[id as keyof typeof rooModels]
 			const info = rooModels[id as keyof typeof rooModels]
 			return { id, info }
 			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 "anthropic":
 		// case "human-relay":
 		// case "human-relay":
 		// case "fake-ai":
 		// case "fake-ai":
 		default: {
 		default: {
-			provider satisfies "anthropic" | "gemini-cli" | "human-relay" | "fake-ai"
+			provider satisfies "anthropic" | "gemini-cli" | "qwen-code" | "human-relay" | "fake-ai"
 			const id = apiConfiguration.apiModelId ?? anthropicDefaultModelId
 			const id = apiConfiguration.apiModelId ?? anthropicDefaultModelId
 			const baseInfo = anthropicModels[id as keyof typeof anthropicModels]
 			const baseInfo = anthropicModels[id as keyof typeof anthropicModels]
 
 

+ 2 - 1
webview-ui/src/i18n/locales/ca/settings.json

@@ -795,7 +795,8 @@
 		"modelAvailability": "L'ID de model ({{modelId}}) que heu proporcionat no està disponible. Si us plau, trieu un altre model.",
 		"modelAvailability": "L'ID de model ({{modelId}}) que heu proporcionat no està disponible. Si us plau, trieu un altre model.",
 		"providerNotAllowed": "El proveïdor '{{provider}}' no està permès per la vostra organització",
 		"providerNotAllowed": "El proveïdor '{{provider}}' no està permès per la vostra organització",
 		"modelNotAllowed": "El model '{{model}}' no està permès per al proveïdor '{{provider}}' per la vostra organització",
 		"modelNotAllowed": "El model '{{model}}' no està permès per al proveïdor '{{provider}}' per la vostra organització",
-		"profileInvalid": "Aquest perfil conté un proveïdor o model que no està permès per la vostra organització"
+		"profileInvalid": "Aquest perfil conté un proveïdor o model que no està permès per la vostra organització",
+		"qwenCodeOauthPath": "Has de proporcionar una ruta vàlida de credencials OAuth"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "Introduïu la clau API...",
 		"apiKey": "Introduïu la clau API...",

+ 2 - 1
webview-ui/src/i18n/locales/de/settings.json

@@ -795,7 +795,8 @@
 		"modelAvailability": "Die von dir angegebene Modell-ID ({{modelId}}) ist nicht verfügbar. Bitte wähle ein anderes Modell.",
 		"modelAvailability": "Die von dir angegebene Modell-ID ({{modelId}}) ist nicht verfügbar. Bitte wähle ein anderes Modell.",
 		"providerNotAllowed": "Anbieter '{{provider}}' ist von deiner Organisation nicht erlaubt",
 		"providerNotAllowed": "Anbieter '{{provider}}' ist von deiner Organisation nicht erlaubt",
 		"modelNotAllowed": "Modell '{{model}}' ist für Anbieter '{{provider}}' von deiner Organisation nicht erlaubt",
 		"modelNotAllowed": "Modell '{{model}}' ist für Anbieter '{{provider}}' von deiner Organisation nicht erlaubt",
-		"profileInvalid": "Dieses Profil enthält einen Anbieter oder ein Modell, das von deiner Organisation nicht erlaubt ist"
+		"profileInvalid": "Dieses Profil enthält einen Anbieter oder ein Modell, das von deiner Organisation nicht erlaubt ist",
+		"qwenCodeOauthPath": "Du musst einen gültigen OAuth-Anmeldedaten-Pfad angeben"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "API-Schlüssel eingeben...",
 		"apiKey": "API-Schlüssel eingeben...",

+ 2 - 1
webview-ui/src/i18n/locales/en/settings.json

@@ -794,7 +794,8 @@
 		"modelAvailability": "The model ID ({{modelId}}) you provided is not available. Please choose a different model.",
 		"modelAvailability": "The model ID ({{modelId}}) you provided is not available. Please choose a different model.",
 		"providerNotAllowed": "Provider '{{provider}}' is not allowed by your organization",
 		"providerNotAllowed": "Provider '{{provider}}' is not allowed by your organization",
 		"modelNotAllowed": "Model '{{model}}' is not allowed for provider '{{provider}}' by your organization",
 		"modelNotAllowed": "Model '{{model}}' is not allowed for provider '{{provider}}' by your organization",
-		"profileInvalid": "This profile contains a provider or model that is not allowed by your organization"
+		"profileInvalid": "This profile contains a provider or model that is not allowed by your organization",
+		"qwenCodeOauthPath": "You must provide a valid OAuth credentials path."
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "Enter API Key...",
 		"apiKey": "Enter API Key...",

+ 2 - 1
webview-ui/src/i18n/locales/es/settings.json

@@ -795,7 +795,8 @@
 		"modelAvailability": "El ID de modelo ({{modelId}}) que proporcionó no está disponible. Por favor, elija un modelo diferente.",
 		"modelAvailability": "El ID de modelo ({{modelId}}) que proporcionó no está disponible. Por favor, elija un modelo diferente.",
 		"providerNotAllowed": "El proveedor '{{provider}}' no está permitido por su organización",
 		"providerNotAllowed": "El proveedor '{{provider}}' no está permitido por su organización",
 		"modelNotAllowed": "El modelo '{{model}}' no está permitido para el proveedor '{{provider}}' por su organización",
 		"modelNotAllowed": "El modelo '{{model}}' no está permitido para el proveedor '{{provider}}' por su organización",
-		"profileInvalid": "Este perfil contiene un proveedor o modelo que no está permitido por su organización"
+		"profileInvalid": "Este perfil contiene un proveedor o modelo que no está permitido por su organización",
+		"qwenCodeOauthPath": "Debes proporcionar una ruta válida de credenciales OAuth"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "Ingrese clave API...",
 		"apiKey": "Ingrese clave API...",

+ 2 - 1
webview-ui/src/i18n/locales/fr/settings.json

@@ -795,7 +795,8 @@
 		"modelAvailability": "L'ID de modèle ({{modelId}}) que vous avez fourni n'est pas disponible. Veuillez choisir un modèle différent.",
 		"modelAvailability": "L'ID de modèle ({{modelId}}) que vous avez fourni n'est pas disponible. Veuillez choisir un modèle différent.",
 		"providerNotAllowed": "Le fournisseur '{{provider}}' n'est pas autorisé par votre organisation",
 		"providerNotAllowed": "Le fournisseur '{{provider}}' n'est pas autorisé par votre organisation",
 		"modelNotAllowed": "Le modèle '{{model}}' n'est pas autorisé pour le fournisseur '{{provider}}' par votre organisation",
 		"modelNotAllowed": "Le modèle '{{model}}' n'est pas autorisé pour le fournisseur '{{provider}}' par votre organisation",
-		"profileInvalid": "Ce profil contient un fournisseur ou un modèle qui n'est pas autorisé par votre organisation"
+		"profileInvalid": "Ce profil contient un fournisseur ou un modèle qui n'est pas autorisé par votre organisation",
+		"qwenCodeOauthPath": "Tu dois fournir un chemin valide pour les identifiants OAuth"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "Saisissez la clé API...",
 		"apiKey": "Saisissez la clé API...",

+ 2 - 1
webview-ui/src/i18n/locales/hi/settings.json

@@ -796,7 +796,8 @@
 		"modelAvailability": "आपके द्वारा प्रदान की गई मॉडल ID ({{modelId}}) उपलब्ध नहीं है। कृपया कोई अन्य मॉडल चुनें।",
 		"modelAvailability": "आपके द्वारा प्रदान की गई मॉडल ID ({{modelId}}) उपलब्ध नहीं है। कृपया कोई अन्य मॉडल चुनें।",
 		"providerNotAllowed": "प्रदाता '{{provider}}' आपके संगठन द्वारा अनुमत नहीं है",
 		"providerNotAllowed": "प्रदाता '{{provider}}' आपके संगठन द्वारा अनुमत नहीं है",
 		"modelNotAllowed": "मॉडल '{{model}}' प्रदाता '{{provider}}' के लिए आपके संगठन द्वारा अनुमत नहीं है",
 		"modelNotAllowed": "मॉडल '{{model}}' प्रदाता '{{provider}}' के लिए आपके संगठन द्वारा अनुमत नहीं है",
-		"profileInvalid": "इस प्रोफ़ाइल में एक प्रदाता या मॉडल शामिल है जो आपके संगठन द्वारा अनुमत नहीं है"
+		"profileInvalid": "इस प्रोफ़ाइल में एक प्रदाता या मॉडल शामिल है जो आपके संगठन द्वारा अनुमत नहीं है",
+		"qwenCodeOauthPath": "आपको एक वैध OAuth क्रेडेंशियल पथ प्रदान करना होगा"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "API कुंजी दर्ज करें...",
 		"apiKey": "API कुंजी दर्ज करें...",

+ 2 - 1
webview-ui/src/i18n/locales/id/settings.json

@@ -825,7 +825,8 @@
 		"modelAvailability": "Model ID ({{modelId}}) yang kamu berikan tidak tersedia. Silakan pilih model yang berbeda.",
 		"modelAvailability": "Model ID ({{modelId}}) yang kamu berikan tidak tersedia. Silakan pilih model yang berbeda.",
 		"providerNotAllowed": "Provider '{{provider}}' tidak diizinkan oleh organisasi kamu",
 		"providerNotAllowed": "Provider '{{provider}}' tidak diizinkan oleh organisasi kamu",
 		"modelNotAllowed": "Model '{{model}}' tidak diizinkan untuk provider '{{provider}}' oleh organisasi kamu",
 		"modelNotAllowed": "Model '{{model}}' tidak diizinkan untuk provider '{{provider}}' oleh organisasi kamu",
-		"profileInvalid": "Profil ini berisi provider atau model yang tidak diizinkan oleh organisasi kamu"
+		"profileInvalid": "Profil ini berisi provider atau model yang tidak diizinkan oleh organisasi kamu",
+		"qwenCodeOauthPath": "Kamu harus memberikan jalur kredensial OAuth yang valid"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "Masukkan API Key...",
 		"apiKey": "Masukkan API Key...",

+ 2 - 1
webview-ui/src/i18n/locales/it/settings.json

@@ -796,7 +796,8 @@
 		"modelAvailability": "L'ID modello ({{modelId}}) fornito non è disponibile. Seleziona un modello diverso.",
 		"modelAvailability": "L'ID modello ({{modelId}}) fornito non è disponibile. Seleziona un modello diverso.",
 		"providerNotAllowed": "Il fornitore '{{provider}}' non è consentito dalla tua organizzazione",
 		"providerNotAllowed": "Il fornitore '{{provider}}' non è consentito dalla tua organizzazione",
 		"modelNotAllowed": "Il modello '{{model}}' non è consentito per il fornitore '{{provider}}' dalla tua organizzazione.",
 		"modelNotAllowed": "Il modello '{{model}}' non è consentito per il fornitore '{{provider}}' dalla tua organizzazione.",
-		"profileInvalid": "Questo profilo contiene un fornitore o un modello non consentito dalla tua organizzazione."
+		"profileInvalid": "Questo profilo contiene un fornitore o un modello non consentito dalla tua organizzazione.",
+		"qwenCodeOauthPath": "Devi fornire un percorso valido per le credenziali OAuth"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "Inserisci chiave API...",
 		"apiKey": "Inserisci chiave API...",

+ 2 - 1
webview-ui/src/i18n/locales/ja/settings.json

@@ -796,7 +796,8 @@
 		"modelAvailability": "指定されたモデルID({{modelId}})は利用できません。別のモデルを選択してください。",
 		"modelAvailability": "指定されたモデルID({{modelId}})は利用できません。別のモデルを選択してください。",
 		"providerNotAllowed": "プロバイダー「{{provider}}」は組織によって許可されていません",
 		"providerNotAllowed": "プロバイダー「{{provider}}」は組織によって許可されていません",
 		"modelNotAllowed": "モデル「{{model}}」はプロバイダー「{{provider}}」に対して組織によって許可されていません",
 		"modelNotAllowed": "モデル「{{model}}」はプロバイダー「{{provider}}」に対して組織によって許可されていません",
-		"profileInvalid": "このプロファイルには、組織によって許可されていないプロバイダーまたはモデルが含まれています"
+		"profileInvalid": "このプロファイルには、組織によって許可されていないプロバイダーまたはモデルが含まれています",
+		"qwenCodeOauthPath": "有効なOAuth認証情報のパスを提供する必要があります"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "API キーを入力...",
 		"apiKey": "API キーを入力...",

+ 2 - 1
webview-ui/src/i18n/locales/ko/settings.json

@@ -796,7 +796,8 @@
 		"modelAvailability": "제공한 모델 ID({{modelId}})를 사용할 수 없습니다. 다른 모델을 선택하세요.",
 		"modelAvailability": "제공한 모델 ID({{modelId}})를 사용할 수 없습니다. 다른 모델을 선택하세요.",
 		"providerNotAllowed": "제공자 '{{provider}}'는 조직에서 허용되지 않습니다",
 		"providerNotAllowed": "제공자 '{{provider}}'는 조직에서 허용되지 않습니다",
 		"modelNotAllowed": "모델 '{{model}}'은 제공자 '{{provider}}'에 대해 조직에서 허용되지 않습니다",
 		"modelNotAllowed": "모델 '{{model}}'은 제공자 '{{provider}}'에 대해 조직에서 허용되지 않습니다",
-		"profileInvalid": "이 프로필에는 조직에서 허용되지 않는 제공자 또는 모델이 포함되어 있습니다"
+		"profileInvalid": "이 프로필에는 조직에서 허용되지 않는 제공자 또는 모델이 포함되어 있습니다",
+		"qwenCodeOauthPath": "유효한 OAuth 자격 증명 경로를 제공해야 합니다"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "API 키 입력...",
 		"apiKey": "API 키 입력...",

+ 2 - 1
webview-ui/src/i18n/locales/nl/settings.json

@@ -796,7 +796,8 @@
 		"modelAvailability": "Het opgegeven model-ID ({{modelId}}) is niet beschikbaar. Kies een ander model.",
 		"modelAvailability": "Het opgegeven model-ID ({{modelId}}) is niet beschikbaar. Kies een ander model.",
 		"providerNotAllowed": "Provider '{{provider}}' is niet toegestaan door je organisatie",
 		"providerNotAllowed": "Provider '{{provider}}' is niet toegestaan door je organisatie",
 		"modelNotAllowed": "Model '{{model}}' is niet toegestaan voor provider '{{provider}}' door je organisatie",
 		"modelNotAllowed": "Model '{{model}}' is niet toegestaan voor provider '{{provider}}' door je organisatie",
-		"profileInvalid": "Dit profiel bevat een provider of model dat niet is toegestaan door je organisatie"
+		"profileInvalid": "Dit profiel bevat een provider of model dat niet is toegestaan door je organisatie",
+		"qwenCodeOauthPath": "Je moet een geldig OAuth-referentiepad opgeven"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "Voer API-sleutel in...",
 		"apiKey": "Voer API-sleutel in...",

+ 2 - 1
webview-ui/src/i18n/locales/pl/settings.json

@@ -796,7 +796,8 @@
 		"modelAvailability": "Podane ID modelu ({{modelId}}) jest niedostępne. Wybierz inny model.",
 		"modelAvailability": "Podane ID modelu ({{modelId}}) jest niedostępne. Wybierz inny model.",
 		"providerNotAllowed": "Dostawca '{{provider}}' nie jest dozwolony przez Twoją organizację",
 		"providerNotAllowed": "Dostawca '{{provider}}' nie jest dozwolony przez Twoją organizację",
 		"modelNotAllowed": "Model '{{model}}' nie jest dozwolony dla dostawcy '{{provider}}' przez Twoją organizację",
 		"modelNotAllowed": "Model '{{model}}' nie jest dozwolony dla dostawcy '{{provider}}' przez Twoją organizację",
-		"profileInvalid": "Ten profil zawiera dostawcę lub model, który nie jest dozwolony przez Twoją organizację"
+		"profileInvalid": "Ten profil zawiera dostawcę lub model, który nie jest dozwolony przez Twoją organizację",
+		"qwenCodeOauthPath": "Musisz podać prawidłową ścieżkę do poświadczeń OAuth"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "Wprowadź klucz API...",
 		"apiKey": "Wprowadź klucz API...",

+ 2 - 1
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -796,7 +796,8 @@
 		"modelAvailability": "O ID do modelo ({{modelId}}) que você forneceu não está disponível. Por favor, escolha outro modelo.",
 		"modelAvailability": "O ID do modelo ({{modelId}}) que você forneceu não está disponível. Por favor, escolha outro modelo.",
 		"providerNotAllowed": "O provedor '{{provider}}' não é permitido pela sua organização",
 		"providerNotAllowed": "O provedor '{{provider}}' não é permitido pela sua organização",
 		"modelNotAllowed": "O modelo '{{model}}' não é permitido para o provedor '{{provider}}' pela sua organização",
 		"modelNotAllowed": "O modelo '{{model}}' não é permitido para o provedor '{{provider}}' pela sua organização",
-		"profileInvalid": "Este perfil contém um provedor ou modelo que não é permitido pela sua organização"
+		"profileInvalid": "Este perfil contém um provedor ou modelo que não é permitido pela sua organização",
+		"qwenCodeOauthPath": "Você deve fornecer um caminho válido de credenciais OAuth"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "Digite a chave API...",
 		"apiKey": "Digite a chave API...",

+ 2 - 1
webview-ui/src/i18n/locales/ru/settings.json

@@ -796,7 +796,8 @@
 		"modelAvailability": "ID модели ({{modelId}}), который вы указали, недоступен. Пожалуйста, выберите другую модель.",
 		"modelAvailability": "ID модели ({{modelId}}), который вы указали, недоступен. Пожалуйста, выберите другую модель.",
 		"providerNotAllowed": "Провайдер '{{provider}}' не разрешен вашей организацией",
 		"providerNotAllowed": "Провайдер '{{provider}}' не разрешен вашей организацией",
 		"modelNotAllowed": "Модель '{{model}}' не разрешена для провайдера '{{provider}}' вашей организацией",
 		"modelNotAllowed": "Модель '{{model}}' не разрешена для провайдера '{{provider}}' вашей организацией",
-		"profileInvalid": "Этот профиль содержит провайдера или модель, которые не разрешены вашей организацией"
+		"profileInvalid": "Этот профиль содержит провайдера или модель, которые не разрешены вашей организацией",
+		"qwenCodeOauthPath": "Вы должны указать допустимый путь к учетным данным OAuth"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "Введите API-ключ...",
 		"apiKey": "Введите API-ключ...",

+ 2 - 1
webview-ui/src/i18n/locales/tr/settings.json

@@ -796,7 +796,8 @@
 		"modelAvailability": "Sağladığınız model kimliği ({{modelId}}) kullanılamıyor. Lütfen başka bir model seçin.",
 		"modelAvailability": "Sağladığınız model kimliği ({{modelId}}) kullanılamıyor. Lütfen başka bir model seçin.",
 		"providerNotAllowed": "Sağlayıcı '{{provider}}' kuruluşunuz tarafından izin verilmiyor",
 		"providerNotAllowed": "Sağlayıcı '{{provider}}' kuruluşunuz tarafından izin verilmiyor",
 		"modelNotAllowed": "Model '{{model}}' sağlayıcı '{{provider}}' için kuruluşunuz tarafından izin verilmiyor",
 		"modelNotAllowed": "Model '{{model}}' sağlayıcı '{{provider}}' için kuruluşunuz tarafından izin verilmiyor",
-		"profileInvalid": "Bu profil, kuruluşunuz tarafından izin verilmeyen bir sağlayıcı veya model içeriyor"
+		"profileInvalid": "Bu profil, kuruluşunuz tarafından izin verilmeyen bir sağlayıcı veya model içeriyor",
+		"qwenCodeOauthPath": "Geçerli bir OAuth kimlik bilgileri yolu sağlamalısın"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "API anahtarını girin...",
 		"apiKey": "API anahtarını girin...",

+ 2 - 1
webview-ui/src/i18n/locales/vi/settings.json

@@ -796,7 +796,8 @@
 		"modelAvailability": "ID mô hình ({{modelId}}) bạn đã cung cấp không khả dụng. Vui lòng chọn một mô hình khác.",
 		"modelAvailability": "ID mô hình ({{modelId}}) bạn đã cung cấp không khả dụng. Vui lòng chọn một mô hình khác.",
 		"providerNotAllowed": "Nhà cung cấp '{{provider}}' không được phép bởi tổ chức của bạn",
 		"providerNotAllowed": "Nhà cung cấp '{{provider}}' không được phép bởi tổ chức của bạn",
 		"modelNotAllowed": "Mô hình '{{model}}' không được phép cho nhà cung cấp '{{provider}}' bởi tổ chức của bạn",
 		"modelNotAllowed": "Mô hình '{{model}}' không được phép cho nhà cung cấp '{{provider}}' bởi tổ chức của bạn",
-		"profileInvalid": "Hồ sơ này chứa một nhà cung cấp hoặc mô hình không được phép bởi tổ chức của bạn"
+		"profileInvalid": "Hồ sơ này chứa một nhà cung cấp hoặc mô hình không được phép bởi tổ chức của bạn",
+		"qwenCodeOauthPath": "Bạn phải cung cấp đường dẫn thông tin xác thực OAuth hợp lệ"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "Nhập khóa API...",
 		"apiKey": "Nhập khóa API...",

+ 2 - 1
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -796,7 +796,8 @@
 		"modelAvailability": "模型ID {{modelId}} 不可用,请重新选择",
 		"modelAvailability": "模型ID {{modelId}} 不可用,请重新选择",
 		"providerNotAllowed": "提供商 '{{provider}}' 不允许用于您的组织",
 		"providerNotAllowed": "提供商 '{{provider}}' 不允许用于您的组织",
 		"modelNotAllowed": "模型 '{{model}}' 不允许用于提供商 '{{provider}}',您的组织不允许",
 		"modelNotAllowed": "模型 '{{model}}' 不允许用于提供商 '{{provider}}',您的组织不允许",
-		"profileInvalid": "此配置文件包含您的组织不允许的提供商或模型"
+		"profileInvalid": "此配置文件包含您的组织不允许的提供商或模型",
+		"qwenCodeOauthPath": "您必须提供有效的 OAuth 凭证路径"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "请输入 API 密钥...",
 		"apiKey": "请输入 API 密钥...",

+ 2 - 1
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -796,7 +796,8 @@
 		"modelAvailability": "您指定的模型 ID ({{modelId}}) 目前無法使用,請選擇其他模型。",
 		"modelAvailability": "您指定的模型 ID ({{modelId}}) 目前無法使用,請選擇其他模型。",
 		"providerNotAllowed": "供應商 '{{provider}}' 不允許用於您的組織。",
 		"providerNotAllowed": "供應商 '{{provider}}' 不允許用於您的組織。",
 		"modelNotAllowed": "模型 '{{model}}' 不允許用於供應商 '{{provider}}',您的組織不允許",
 		"modelNotAllowed": "模型 '{{model}}' 不允許用於供應商 '{{provider}}',您的組織不允許",
-		"profileInvalid": "此設定檔包含您的組織不允許的供應商或模型"
+		"profileInvalid": "此設定檔包含您的組織不允許的供應商或模型",
+		"qwenCodeOauthPath": "您必須提供有效的 OAuth 憑證路徑"
 	},
 	},
 	"placeholders": {
 	"placeholders": {
 		"apiKey": "請輸入 API 金鑰...",
 		"apiKey": "請輸入 API 金鑰...",

+ 5 - 0
webview-ui/src/utils/validate.ts

@@ -131,6 +131,11 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri
 				return i18next.t("settings:validation.apiKey")
 				return i18next.t("settings:validation.apiKey")
 			}
 			}
 			break
 			break
+		case "qwen-code":
+			if (!apiConfiguration.qwenCodeOauthPath) {
+				return i18next.t("settings:validation.qwenCodeOauthPath")
+			}
+			break
 	}
 	}
 
 
 	return undefined
 	return undefined