diff --git a/open-sse/config/appConstants.js b/open-sse/config/appConstants.js index 5e3ec4be..62e2f888 100644 --- a/open-sse/config/appConstants.js +++ b/open-sse/config/appConstants.js @@ -176,7 +176,6 @@ export const OAUTH_ENDPOINTS = { google: { token: "https://oauth2.googleapis.com/token", auth: "https://accounts.google.com/o/oauth2/auth" }, openai: { token: PROVIDER_OAUTH["codex"]?.tokenUrl, auth: PROVIDER_OAUTH["codex"]?.authorizeUrl }, anthropic: { token: PROVIDER_OAUTH["claude"]?.tokenUrl, auth: "https://api.anthropic.com/v1/oauth/authorize" }, // ≠ claude.authorizeUrl (claude.ai login) — keep - qwen: { token: PROVIDER_OAUTH["qwen"]?.tokenUrl, auth: PROVIDER_OAUTH["qwen"]?.deviceCodeUrl }, iflow: { token: PROVIDER_OAUTH["iflow"]?.tokenUrl, auth: PROVIDER_OAUTH["iflow"]?.authorizeUrl }, github: { token: PROVIDER_OAUTH["github"]?.tokenUrl, auth: PROVIDER_OAUTH["github"]?.authorizeUrl, deviceCode: PROVIDER_OAUTH["github"]?.deviceCodeUrl }, }; diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js index eb000f37..92c78e92 100644 --- a/open-sse/executors/default.js +++ b/open-sse/executors/default.js @@ -211,7 +211,6 @@ export class DefaultExecutor extends BaseExecutor { const refreshers = { claude: () => this.refreshFromGrant(credentials, proxyOptions), codex: () => this.refreshFromGrant(credentials, proxyOptions), - qwen: () => this.refreshWithForm(OAUTH_ENDPOINTS.qwen.token, { grant_type: "refresh_token", refresh_token: credentials.refreshToken, client_id: PROVIDERS.qwen.clientId }, proxyOptions), iflow: () => this.refreshIflow(credentials.refreshToken, proxyOptions), gemini: () => this.refreshFromGrant(credentials, proxyOptions), kiro: () => this.refreshKiro(credentials.refreshToken, proxyOptions), diff --git a/open-sse/executors/index.js b/open-sse/executors/index.js index 92e10464..8dd03421 100644 --- a/open-sse/executors/index.js +++ b/open-sse/executors/index.js @@ -9,7 +9,6 @@ import { KimchiExecutor } from "./kimchi.js"; import { CodexExecutor } from "./codex.js"; import { CursorExecutor } from "./cursor.js"; import { VertexExecutor } from "./vertex.js"; -import { QwenExecutor } from "./qwen.js"; import { OpenCodeExecutor } from "./opencode.js"; import { OpenCodeGoExecutor } from "./opencode-go.js"; import { GrokWebExecutor } from "./grok-web.js"; @@ -41,7 +40,6 @@ const executors = { cu: new CursorExecutor(), // Alias for cursor vertex: new VertexExecutor("vertex"), "vertex-partner": new VertexExecutor("vertex-partner"), - qwen: new QwenExecutor(), opencode: new OpenCodeExecutor(), "opencode-go": new OpenCodeGoExecutor(), "grok-web": new GrokWebExecutor(), @@ -87,7 +85,6 @@ export { CodexExecutor } from "./codex.js"; export { CursorExecutor } from "./cursor.js"; export { VertexExecutor } from "./vertex.js"; export { DefaultExecutor } from "./default.js"; -export { QwenExecutor } from "./qwen.js"; export { OpenCodeExecutor } from "./opencode.js"; export { OpenCodeGoExecutor } from "./opencode-go.js"; export { GrokWebExecutor } from "./grok-web.js"; diff --git a/open-sse/executors/qwen.js b/open-sse/executors/qwen.js deleted file mode 100644 index ae828118..00000000 --- a/open-sse/executors/qwen.js +++ /dev/null @@ -1,129 +0,0 @@ -import { DefaultExecutor } from "./default.js"; -import { PROVIDERS } from "../config/providers.js"; -import { OAUTH_ENDPOINTS } from "../config/appConstants.js"; - -/** portal.qwen.ai — static fingerprint matching stable Qwen Code release */ -const QWEN_USER_AGENT = "QwenCode/0.12.3 (linux; x64)"; -const QWEN_STAINLESS = { - os: "Linux", - arch: "x64", - lang: "js", - runtime: "node", - runtimeVersion: "v18.19.1", - packageVersion: "5.11.0", - retryCount: "1" -}; -const QWEN_DEFAULT_SYSTEM_MESSAGE = { - role: "system", - content: [{ type: "text", text: "", cache_control: { type: "ephemeral" } }] -}; - -function ensureQwenSystemMessage(body) { - if (!body || typeof body !== "object") return body; - const next = { ...body }; - if (Array.isArray(next.messages)) { - next.messages = [QWEN_DEFAULT_SYSTEM_MESSAGE, ...next.messages]; - } else { - next.messages = [QWEN_DEFAULT_SYSTEM_MESSAGE]; - } - return next; -} - -function isQwenThinkingActive(body) { - const thinking = body?.thinking; - if (thinking === true || body?.enable_thinking === true) return true; - return typeof thinking === "object" && thinking !== null && !Array.isArray(thinking) && thinking.type === "enabled"; -} - -// Qwen rejects tool_choice="required" or object forms when thinking is active; neutralize to "auto". -function sanitizeQwenThinkingToolChoice(body) { - if (!isQwenThinkingActive(body)) return body; - const tc = body.tool_choice; - const incompatible = tc === "required" || (typeof tc === "object" && tc !== null); - if (!incompatible) return body; - return { ...body, tool_choice: "auto" }; -} - -function buildQwenUpstreamHeaders(credentials, stream = true) { - const token = credentials?.apiKey || credentials?.accessToken || ""; - const headers = { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - "User-Agent": QWEN_USER_AGENT, - "X-DashScope-AuthType": "qwen-oauth", - "X-DashScope-CacheControl": "enable", - "X-DashScope-UserAgent": QWEN_USER_AGENT, - "X-Stainless-Arch": QWEN_STAINLESS.arch, - "X-Stainless-Lang": QWEN_STAINLESS.lang, - "X-Stainless-Os": QWEN_STAINLESS.os, - "X-Stainless-Package-Version": QWEN_STAINLESS.packageVersion, - "X-Stainless-Retry-Count": QWEN_STAINLESS.retryCount, - "X-Stainless-Runtime": QWEN_STAINLESS.runtime, - "X-Stainless-Runtime-Version": QWEN_STAINLESS.runtimeVersion, - Connection: "keep-alive", - "Accept-Language": "*", - "Sec-Fetch-Mode": "cors" - }; - headers.Accept = stream ? "text/event-stream" : "application/json"; - return headers; -} - -export class QwenExecutor extends DefaultExecutor { - constructor() { - super("qwen"); - } - - // Qwen tokens are bound to a resource_url returned at OAuth time. - // Using portal.qwen.ai when the token is issued for another shard returns 401/403. - buildUrl(model, stream, urlIndex = 0, credentials = null) { - const resourceUrl = credentials?.providerSpecificData?.resourceUrl; - const host = resourceUrl ? resourceUrl.replace(/^https?:\/\//, "").replace(/\/$/, "") : "portal.qwen.ai"; - return `https://${host}/v1/chat/completions`; - } - - buildHeaders(credentials, stream = true) { - return buildQwenUpstreamHeaders(credentials, stream); - } - - transformRequest(model, body, stream, credentials) { - let next = body && typeof body === "object" ? { ...body } : body; - if (stream && next?.messages && !next.stream_options && !next.thinking && !next.enable_thinking && next.stream !== false) { - next.stream_options = { include_usage: true }; - } - next = sanitizeQwenThinkingToolChoice(next); - return ensureQwenSystemMessage(next); - } - - // Override to capture resource_url from refresh response (required for buildUrl). - async refreshCredentials(credentials, log) { - if (!credentials?.refreshToken) return null; - try { - const response = await fetch(OAUTH_ENDPOINTS.qwen.token, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: credentials.refreshToken, - client_id: PROVIDERS.qwen.clientId - }) - }); - if (!response.ok) return null; - const tokens = await response.json(); - log?.info?.("TOKEN", "qwen refreshed"); - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || credentials.refreshToken, - expiresIn: tokens.expires_in, - providerSpecificData: { - ...(credentials.providerSpecificData || {}), - ...(tokens.resource_url ? { resourceUrl: tokens.resource_url } : {}) - } - }; - } catch (error) { - log?.error?.("TOKEN", `qwen refresh error: ${error.message}`); - return null; - } - } -} - -export default QwenExecutor; diff --git a/open-sse/index.js b/open-sse/index.js index b8181f0b..c0694580 100644 --- a/open-sse/index.js +++ b/open-sse/index.js @@ -47,7 +47,6 @@ export { refreshAccessToken, refreshClaudeOAuthToken, refreshGoogleToken, - refreshQwenToken, refreshCodexToken, refreshIflowToken, refreshGitHubToken, diff --git a/open-sse/providers/registry/index.js b/open-sse/providers/registry/index.js index c08c0597..467d1c4d 100644 --- a/open-sse/providers/registry/index.js +++ b/open-sse/providers/registry/index.js @@ -75,7 +75,6 @@ import p72 from "./perplexity.js"; import p73 from "./perplexity-agent.js"; import p74 from "./playht.js"; import p75 from "./qoder.js"; -import p76 from "./qwen.js"; import p77 from "./recraft.js"; import p78 from "./runwayml.js"; import p79 from "./sdwebui.js"; @@ -198,7 +197,6 @@ export default [ p73, p74, p75, - p76, p77, p78, p79, diff --git a/open-sse/providers/registry/qwen.js b/open-sse/providers/registry/qwen.js deleted file mode 100644 index 0df381ab..00000000 --- a/open-sse/providers/registry/qwen.js +++ /dev/null @@ -1,33 +0,0 @@ -export default { - id: "qwen", - hidden: true, - priority: 130, - alias: "qw", - display: { - name: "Qwen Code", - icon: "psychology", - color: "#10B981", - website: "https://chat.qwen.ai", - notice: { - signupUrl: "https://chat.qwen.ai", - }, - }, - category: "oauth", - transport: { - baseUrl: "https://portal.qwen.ai/v1/chat/completions", - }, - models: [ - { id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" }, - { id: "qwen3-coder-flash", name: "Qwen3 Coder Flash" }, - { id: "vision-model", name: "Qwen3 Vision Model" }, - { id: "coder-model", name: "Qwen3.6 Coder Model" }, - ], - oauth: { - clientId: "f0304373b74a44d2b584a3fb70ca9e56", - deviceCodeUrl: "https://chat.qwen.ai/api/v1/oauth2/device/code", - tokenUrl: "https://chat.qwen.ai/api/v1/oauth2/token", - scope: "openid profile email model.completion", - codeChallengeMethod: "S256", - refreshLeadMs: 1200000, - }, -}; diff --git a/open-sse/services/tokenRefresh.js b/open-sse/services/tokenRefresh.js index 634fd633..3160f4a7 100644 --- a/open-sse/services/tokenRefresh.js +++ b/open-sse/services/tokenRefresh.js @@ -6,7 +6,6 @@ import { refreshKimiToken, refreshClaudeOAuthToken, refreshGoogleToken, - refreshQwenToken, refreshCodexToken, refreshKiroToken, refreshIflowToken, @@ -26,7 +25,6 @@ export { refreshKimiToken, refreshClaudeOAuthToken, refreshGoogleToken, - refreshQwenToken, refreshCodexToken, refreshKiroToken, refreshIflowToken, @@ -137,7 +135,6 @@ const REFRESH_HANDLERS = { antigravity: (c, log) => refreshGoogleToken(c.refreshToken, PROVIDERS.antigravity.clientId, PROVIDERS.antigravity.clientSecret, log), claude: (c, log) => refreshClaudeOAuthToken(c.refreshToken, log), codex: (c, log) => refreshCodexToken(c.refreshToken, log), - qwen: (c, log) => refreshQwenToken(c.refreshToken, log), iflow: (c, log) => refreshIflowToken(c.refreshToken, log), github: (c, log) => refreshGitHubToken(c.refreshToken, log), kiro: (c, log) => refreshKiroToken(c.refreshToken, c.providerSpecificData, log), @@ -205,7 +202,6 @@ export function formatProviderCredentials(provider, credentials, log) { }; case "codex": - case "qwen": case "iflow": case "openai": case "openrouter": diff --git a/open-sse/services/tokenRefresh/providers.js b/open-sse/services/tokenRefresh/providers.js index 7c13ae77..40f27f51 100644 --- a/open-sse/services/tokenRefresh/providers.js +++ b/open-sse/services/tokenRefresh/providers.js @@ -40,11 +40,6 @@ const REFRESH_PROFILES = { url: () => OAUTH_ENDPOINTS.anthropic.token, dedupKey: "claude", }, - qwen: { - url: () => OAUTH_ENDPOINTS.qwen.token, - dedupKey: "qwen", - parse: (tokens) => tokens.resource_url ? { providerSpecificData: { resourceUrl: tokens.resource_url } } : {}, - }, iflow: { url: () => OAUTH_ENDPOINTS.iflow.token, dedupKey: "iflow", @@ -191,11 +186,6 @@ export async function refreshGoogleToken(refreshToken, clientId, clientSecret, l }, log); } -// Qwen: form body + clientId, surfaces resource_url. Delegate to refreshAccessToken("qwen", ...). -export async function refreshQwenToken(refreshToken, log) { - return refreshAccessToken("qwen", refreshToken, {}, log); -} - export function classifyOAuthRefreshError(errorText = "", status = 0) { let parsed = null; try { diff --git a/open-sse/services/usage.js b/open-sse/services/usage.js index 93bfeca6..70a38ea6 100644 --- a/open-sse/services/usage.js +++ b/open-sse/services/usage.js @@ -16,7 +16,6 @@ import { getKimiUsage } from "./usage/kimi.js"; import { getDeepseekUsage } from "./usage/deepseek.js"; import { resolveQoderCredentials } from "./qoderModels.js"; import { - getQwenUsage, getIflowUsage, getOllamaUsage, getGlmUsage, @@ -43,7 +42,6 @@ const USAGE_HANDLERS = { const resolved = await resolveQoderCredentials(c, c.proxyOptions).catch(() => null); return getQoderUsage(resolved?.accessToken || c.accessToken, c.proxyOptions); }, - qwen: (c) => getQwenUsage(c.accessToken, c.providerSpecificData), iflow: (c) => getIflowUsage(c.accessToken), ollama: (c) => getOllamaUsage(c.apiKey, c.providerSpecificData, c.proxyOptions), glm: (c) => getGlmUsage(c.apiKey, c.provider, c.proxyOptions), diff --git a/open-sse/services/usage/misc.js b/open-sse/services/usage/misc.js index 6a4323c8..fc133eff 100644 --- a/open-sse/services/usage/misc.js +++ b/open-sse/services/usage/misc.js @@ -1,5 +1,5 @@ /** - * Misc usage handlers (Qwen, iFlow, Ollama, GLM, Vercel AI Gateway, Qoder) + * Misc usage handlers (iFlow, Ollama, GLM, Vercel AI Gateway, Qoder) */ import { proxyAwareFetch } from "../../utils/proxyFetch.js"; @@ -15,23 +15,6 @@ const GLM_QUOTA_URLS = { // Returns { balance: "95.50", total_used: "4.50" } (USD as decimal strings). const VERCEL_AI_GATEWAY_CREDITS_URL = U("vercel-ai-gateway").url; -/** - * Qwen Usage - */ -export async function getQwenUsage(accessToken, providerSpecificData) { - try { - const resourceUrl = providerSpecificData?.resourceUrl; - if (!resourceUrl) { - return { message: "Qwen connected. No resource URL available." }; - } - - // Qwen may have usage endpoint at resource URL - return { message: "Qwen connected. Usage tracked per request." }; - } catch (error) { - return { message: "Unable to fetch Qwen usage." }; - } -} - /** * iFlow Usage */ diff --git a/src/app/api/providers/[id]/models/route.js b/src/app/api/providers/[id]/models/route.js index 018db024..d73ec11d 100644 --- a/src/app/api/providers/[id]/models/route.js +++ b/src/app/api/providers/[id]/models/route.js @@ -79,18 +79,6 @@ const createOpenAIModelsConfig = (url) => ({ parseResponse: parseOpenAIStyleModels }); -const resolveQwenModelsUrl = (connection) => { - const fallback = "https://portal.qwen.ai/v1/models"; - const raw = connection?.providerSpecificData?.resourceUrl; - if (!raw || typeof raw !== "string") return fallback; - const value = raw.trim(); - if (!value) return fallback; - if (value.startsWith("http://") || value.startsWith("https://")) { - return `${value.replace(/\/$/, "")}/models`; - } - return `https://${value.replace(/\/$/, "")}/v1/models`; -}; - const getStaticProviderModels = (providerId) => getModelsByProviderId(providerId).map((model) => ({ ...model, @@ -156,14 +144,6 @@ const PROVIDER_MODELS_CONFIG = { authQuery: "key", // Use query param for API key parseResponse: (data) => data.models || [] }, - qwen: { - url: "https://portal.qwen.ai/v1/models", - method: "GET", - headers: { "Content-Type": "application/json" }, - authHeader: "Authorization", - authPrefix: "Bearer ", - parseResponse: (data) => data.data || [] - }, codex: { customResolver: buildOAuthResolver({ refreshFn: (conn) => refreshCodexToken(conn.refreshToken), @@ -572,9 +552,6 @@ export async function GET(request, { params }) { // Build request URL let url = config.url; - if (connection.provider === "qwen") { - url = resolveQwenModelsUrl(connection); - } if (config.authQuery) { url += `?${config.authQuery}=${token}`; } diff --git a/src/app/api/providers/[id]/test/testUtils.js b/src/app/api/providers/[id]/test/testUtils.js index 8ee8c496..0ea46b2d 100644 --- a/src/app/api/providers/[id]/test/testUtils.js +++ b/src/app/api/providers/[id]/test/testUtils.js @@ -12,7 +12,6 @@ import { GEMINI_CONFIG, ANTIGRAVITY_CONFIG, KIRO_CONFIG, - QWEN_CONFIG, CLAUDE_CONFIG, CLINE_CONFIG, KILOCODE_CONFIG, @@ -62,7 +61,6 @@ const OAUTH_TEST_CONFIG = { method: "GET", noAuth: true, }, - qwen: { checkExpiry: true, refreshable: true }, kiro: { checkExpiry: true, refreshable: true }, qoder: { // Test by hitting Qoder's userinfo endpoint with the device token. @@ -285,21 +283,6 @@ async function refreshOAuthToken(connection) { return { accessToken: data.accessToken, expiresIn: data.expiresIn || 3600, refreshToken: data.refreshToken || refreshToken }; } - if (provider === "qwen") { - const response = await fetch(QWEN_CONFIG.tokenUrl, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: QWEN_CONFIG.clientId, - }), - }); - if (!response.ok) return null; - const data = await response.json(); - return { accessToken: data.access_token, expiresIn: data.expires_in, refreshToken: data.refresh_token || refreshToken }; - } - if (provider === "cline") { const response = await fetch(CLINE_CONFIG.refreshUrl, { method: "POST", diff --git a/src/lib/oauth/constants/oauth.js b/src/lib/oauth/constants/oauth.js index d66e32c5..c9e3cae6 100644 --- a/src/lib/oauth/constants/oauth.js +++ b/src/lib/oauth/constants/oauth.js @@ -28,9 +28,6 @@ export const CODEX_CONFIG = { ...PROVIDER_OAUTH["codex"] }; // clientId/clientSecret from GOOGLE_OAUTH_CLIENT (shared.js) — not stored in registry export const GEMINI_CONFIG = { ...GOOGLE_OAUTH_CLIENT, ...PROVIDER_OAUTH["gemini-cli"] }; -// Qwen OAuth Configuration (Device Code Flow with PKCE) -export const QWEN_CONFIG = { ...PROVIDER_OAUTH["qwen"] }; - // Qoder OAuth Configuration (Device Token Flow with PKCE). // Device tokens are long-lived (~30 days for access, ~360 for refresh). // The upstream refresh endpoint at center.qoder.sh returns 403 for our @@ -206,7 +203,6 @@ export const PROVIDERS = { CLAUDE: "claude", CODEX: "codex", GEMINI: "gemini-cli", - QWEN: "qwen", QODER: "qoder", IFLOW: "iflow", ANTIGRAVITY: "antigravity", diff --git a/src/lib/oauth/providers/index.js b/src/lib/oauth/providers/index.js index 8449ecef..e505b01d 100644 --- a/src/lib/oauth/providers/index.js +++ b/src/lib/oauth/providers/index.js @@ -12,7 +12,6 @@ import geminiCli from "./gemini-cli.js"; import antigravity from "./antigravity.js"; import iflow from "./iflow.js"; import qoder from "./qoder.js"; -import qwen from "./qwen.js"; import github from "./github.js"; import kiro from "./kiro.js"; import cursor from "./cursor.js"; @@ -38,7 +37,6 @@ const PROVIDERS = { antigravity, iflow, qoder, - qwen, github, kiro, cursor, diff --git a/src/lib/oauth/providers/qwen.js b/src/lib/oauth/providers/qwen.js deleted file mode 100644 index e8048654..00000000 --- a/src/lib/oauth/providers/qwen.js +++ /dev/null @@ -1,56 +0,0 @@ -import { QWEN_CONFIG } from "../constants/oauth.js"; - -const qwen = { - config: QWEN_CONFIG, - flowType: "device_code", - requestDeviceCode: async (config, codeChallenge) => { - const response = await fetch(config.deviceCodeUrl, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - client_id: config.clientId, - scope: config.scope, - code_challenge: codeChallenge, - code_challenge_method: config.codeChallengeMethod, - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Device code request failed: ${error}`); - } - - return await response.json(); - }, - pollToken: async (config, deviceCode, codeVerifier) => { - const response = await fetch(config.tokenUrl, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - client_id: config.clientId, - device_code: deviceCode, - code_verifier: codeVerifier, - }), - }); - - return { - ok: response.ok, - data: await response.json(), - }; - }, - mapTokens: (tokens) => ({ - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token, - expiresIn: tokens.expires_in, - providerSpecificData: { resourceUrl: tokens.resource_url }, - }), -}; - -export default qwen; diff --git a/src/lib/oauth/services/index.js b/src/lib/oauth/services/index.js index 33cea03c..f8f0214a 100644 --- a/src/lib/oauth/services/index.js +++ b/src/lib/oauth/services/index.js @@ -6,7 +6,6 @@ export { OAuthService } from "./oauth.js"; export { ClaudeService } from "./claude.js"; export { CodexService } from "./codex.js"; export { GeminiCLIService } from "./gemini.js"; -export { QwenService } from "./qwen.js"; export { IFlowService } from "./iflow.js"; export { QoderService } from "./qoder.js"; export { AntigravityService } from "./antigravity.js"; diff --git a/src/lib/oauth/services/qwen.js b/src/lib/oauth/services/qwen.js deleted file mode 100644 index ee4489ea..00000000 --- a/src/lib/oauth/services/qwen.js +++ /dev/null @@ -1,170 +0,0 @@ -import open from "open"; -import { QWEN_CONFIG } from "../constants/oauth.js"; -import { getServerCredentials } from "../config/index.js"; -import { generatePKCE } from "../utils/pkce.js"; -import { spinner as createSpinner } from "../utils/ui.js"; - -/** - * Qwen OAuth Service - * Uses Device Code Flow with PKCE - */ -export class QwenService { - constructor() { - this.config = QWEN_CONFIG; - } - - /** - * Request device code - */ - async requestDeviceCode(codeChallenge) { - const response = await fetch(this.config.deviceCodeUrl, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - client_id: this.config.clientId, - scope: this.config.scope, - code_challenge: codeChallenge, - code_challenge_method: this.config.codeChallengeMethod, - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Device code request failed: ${error}`); - } - - return await response.json(); - } - - /** - * Poll for token - */ - async pollForToken(deviceCode, codeVerifier, interval = 5) { - const maxAttempts = 60; // 5 minutes - const pollInterval = interval * 1000; - - for (let attempt = 0; attempt < maxAttempts; attempt++) { - await new Promise((r) => setTimeout(r, pollInterval)); - - const response = await fetch(this.config.tokenUrl, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - client_id: this.config.clientId, - device_code: deviceCode, - code_verifier: codeVerifier, - }), - }); - - if (response.ok) { - return await response.json(); - } - - const error = await response.json(); - - if (error.error === "authorization_pending") { - continue; - } else if (error.error === "slow_down") { - await new Promise((r) => setTimeout(r, 5000)); - continue; - } else if (error.error === "expired_token") { - throw new Error("Device code expired"); - } else if (error.error === "access_denied") { - throw new Error("Access denied"); - } else { - throw new Error(error.error_description || error.error); - } - } - - throw new Error("Authorization timeout"); - } - - /** - * Save Qwen tokens to server - */ - async saveTokens(tokens) { - const { server, token, userId } = getServerCredentials(); - - const response = await fetch(`${server}/api/cli/providers/qwen`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - "X-User-Id": userId, - }, - body: JSON.stringify({ - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token, - expiresIn: tokens.expires_in, - resourceUrl: tokens.resource_url, - }), - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to save tokens"); - } - - return await response.json(); - } - - /** - * Complete Qwen OAuth flow - */ - async connect() { - const spinner = createSpinner("Starting Qwen OAuth...").start(); - - try { - spinner.text = "Generating PKCE..."; - - // Generate PKCE - const { codeVerifier, codeChallenge } = generatePKCE(); - - spinner.text = "Requesting device code..."; - - // Request device code - const deviceData = await this.requestDeviceCode(codeChallenge); - - spinner.stop(); - - console.log("\n📋 Please visit the following URL and enter the code:\n"); - console.log(` ${deviceData.verification_uri}\n`); - console.log(` Code: ${deviceData.user_code}\n`); - - // Open browser - if (deviceData.verification_uri_complete) { - await open(deviceData.verification_uri_complete); - } else { - await open(deviceData.verification_uri); - } - - spinner.start("Waiting for authorization..."); - - // Poll for token - const tokens = await this.pollForToken( - deviceData.device_code, - codeVerifier, - deviceData.interval || 5 - ); - - spinner.text = "Saving tokens to server..."; - - // Save tokens to server - await this.saveTokens(tokens); - - spinner.succeed("Qwen connected successfully!"); - return true; - } catch (error) { - spinner.fail(`Failed: ${error.message}`); - throw error; - } - } -} - diff --git a/src/shared/components/OAuthModal.js b/src/shared/components/OAuthModal.js index 808a7a8b..805301da 100644 --- a/src/shared/components/OAuthModal.js +++ b/src/shared/components/OAuthModal.js @@ -226,7 +226,6 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess, // Device code flow providers (must match oauth providers with flowType: "device_code") const deviceCodeProviders = [ "github", - "qwen", "kiro", "kimi", "kimi-coding", diff --git a/src/shared/components/Sidebar.js b/src/shared/components/Sidebar.js index cded1824..812e319e 100644 --- a/src/shared/components/Sidebar.js +++ b/src/shared/components/Sidebar.js @@ -21,7 +21,7 @@ const navItems = [ { href: "/dashboard/endpoint", label: "Endpoint & Key", icon: "api" }, { href: "/dashboard/providers", label: "Providers", icon: "dns" }, // { href: "/dashboard/basic-chat", label: "Basic Chat", icon: "chat" }, // Hidden - { href: "/dashboard/combos", label: "Combos", icon: "layers" }, + { href: "/dashboard/combos", label: "Combo & Vision Adapter", icon: "layers" }, { href: "/dashboard/usage", label: "Usage", icon: "bar_chart" }, { href: "/dashboard/quota", label: "Quota Tracker", icon: "data_usage" }, { href: "/dashboard/token-saver", label: "Token Saver", icon: "savings" }, diff --git a/src/sse/services/tokenRefresh.js b/src/sse/services/tokenRefresh.js index 4e75a8ad..6c808f07 100644 --- a/src/sse/services/tokenRefresh.js +++ b/src/sse/services/tokenRefresh.js @@ -11,7 +11,6 @@ import { refreshAccessToken as _refreshAccessToken, refreshClaudeOAuthToken as _refreshClaudeOAuthToken, refreshGoogleToken as _refreshGoogleToken, - refreshQwenToken as _refreshQwenToken, refreshCodexToken as _refreshCodexToken, refreshIflowToken as _refreshIflowToken, refreshGitHubToken as _refreshGitHubToken, @@ -41,9 +40,6 @@ export const refreshClaudeOAuthToken = (refreshToken) => export const refreshGoogleToken = (refreshToken, clientId, clientSecret) => _refreshGoogleToken(refreshToken, clientId, clientSecret, log); -export const refreshQwenToken = (refreshToken) => - _refreshQwenToken(refreshToken, log); - export const refreshCodexToken = (refreshToken) => _refreshCodexToken(refreshToken, log); diff --git a/tests/__baseline__/oauth-urls-baseline.json b/tests/__baseline__/oauth-urls-baseline.json index b4468678..98a27684 100644 --- a/tests/__baseline__/oauth-urls-baseline.json +++ b/tests/__baseline__/oauth-urls-baseline.json @@ -12,10 +12,6 @@ "token": "https://api.anthropic.com/v1/oauth/token", "auth": "https://api.anthropic.com/v1/oauth/authorize" }, - "qwen": { - "token": "https://chat.qwen.ai/api/v1/oauth2/token", - "auth": "https://chat.qwen.ai/api/v1/oauth2/device/code" - }, "iflow": { "token": "https://iflow.cn/oauth/token", "auth": "https://iflow.cn/oauth" @@ -29,7 +25,6 @@ "tokenUrls": { "claude": "https://api.anthropic.com/v1/oauth/token", "codex": "https://auth.openai.com/oauth/token", - "qwen": "https://chat.qwen.ai/api/v1/oauth2/token", "iflow": "https://iflow.cn/oauth/token", "kiro": "https://prod.us-east-1.auth.desktop.kiro.dev/refreshToken", "xai": "https://auth.x.ai/oauth2/token", @@ -49,7 +44,6 @@ "clientIds": { "claude": "9d1c250a-e61b-44d9-88ed-5944d1962f5e", "codex": "app_EMoamEEZ73f0CkXaXp7hrann", - "qwen": "f0304373b74a44d2b584a3fb70ca9e56", "iflow": "10009311001", "kimi": "17e5f671-d194-4dfb-9706-5516cb48c098", "grok-cli": "b1a00492-073a-47ea-816f-4c329264a828" diff --git a/tests/__baseline__/verify-oauth-urls.mjs b/tests/__baseline__/verify-oauth-urls.mjs index d385d102..a43dc9fc 100644 --- a/tests/__baseline__/verify-oauth-urls.mjs +++ b/tests/__baseline__/verify-oauth-urls.mjs @@ -15,7 +15,6 @@ const resolved = { tokenUrls: { claude: PROVIDERS.claude?.tokenUrl, codex: PROVIDERS.codex?.tokenUrl, - qwen: PROVIDERS.qwen?.tokenUrl, iflow: PROVIDERS.iflow?.tokenUrl, kiro: PROVIDERS.kiro?.tokenUrl, xai: PROVIDERS.xai?.tokenUrl, @@ -25,7 +24,6 @@ const resolved = { kimi: PROVIDERS.kimi?.tokenUrl, }, authUrls: { - qwen: PROVIDERS.qwen?.authUrl, iflow: PROVIDERS.iflow?.authUrl, kiro: PROVIDERS.kiro?.authUrl, }, @@ -38,7 +36,6 @@ const resolved = { clientIds: { claude: PROVIDERS.claude?.clientId, codex: PROVIDERS.codex?.clientId, - qwen: PROVIDERS.qwen?.clientId, iflow: PROVIDERS.iflow?.clientId, kimi: PROVIDERS.kimi?.clientId, "grok-cli": PROVIDERS["grok-cli"]?.clientId, diff --git a/tests/translator/golden-url-header.test.js b/tests/translator/golden-url-header.test.js index b9f6bff3..5e92bdf0 100644 --- a/tests/translator/golden-url-header.test.js +++ b/tests/translator/golden-url-header.test.js @@ -18,7 +18,7 @@ const SPECIAL_CRED = { // Chúng được lock riêng ở 11-provider edge tests / unit test chuyên biệt. const SPECIALIZED = new Set([ "antigravity", "azure", "gemini-cli", "github", "iflow", "qoder", "kiro", - "codex", "cursor", "vertex", "vertex-partner", "qwen", "opencode", + "codex", "cursor", "vertex", "vertex-partner", "opencode", "opencode-go", "grok-web", "perplexity-web", "ollama-local", "commandcode", "xiaomi-tokenplan", "mimo-free", ]); diff --git a/tests/unit/codex-refresh-token.test.js b/tests/unit/codex-refresh-token.test.js index e6274c16..4be86e64 100644 --- a/tests/unit/codex-refresh-token.test.js +++ b/tests/unit/codex-refresh-token.test.js @@ -151,7 +151,6 @@ describe("Codex Refresh Token", () => { expect(getRefreshLeadMs("codex")).toBe(5 * 24 * 60 * 60 * 1000); // 5 days expect(getRefreshLeadMs("claude")).toBe(4 * 60 * 60 * 1000); // 4 hours expect(getRefreshLeadMs("iflow")).toBe(24 * 60 * 60 * 1000); // 24 hours - expect(getRefreshLeadMs("qwen")).toBe(20 * 60 * 1000); // 20 minutes expect(getRefreshLeadMs("kimi")).toBe(5 * 60 * 1000); // 5 minutes expect(getRefreshLeadMs("kimi-coding")).toBe(5 * 60 * 1000); // legacy alias expect(getRefreshLeadMs("antigravity")).toBe(5 * 60 * 1000); // 5 minutes diff --git a/tests/unit/token-refresh-generic.test.js b/tests/unit/token-refresh-generic.test.js index d4507c2e..e14c9038 100644 --- a/tests/unit/token-refresh-generic.test.js +++ b/tests/unit/token-refresh-generic.test.js @@ -1,8 +1,8 @@ /** * Generic OAuth2 token refresh — config-driven profiles. * - * Verifies refreshAccessToken() handles the 5 foldable providers - * (qwen, iflow, github, kimi, claude) via a REFRESH_PROFILES table, + * Verifies refreshAccessToken() handles the 4 foldable providers + * (iflow, github, kimi, claude) via a REFRESH_PROFILES table, * while preserving the legacy generic path for unknown providers. */ @@ -25,32 +25,6 @@ describe("refreshAccessToken — config-driven profiles", () => { beforeEach(() => { vi.clearAllMocks(); vi.resetModules(); global.fetch = originalFetch; }); afterEach(() => { global.fetch = originalFetch; }); - it("qwen: form body + clientId, surfaces resource_url as providerSpecificData", async () => { - const fm = mockFetchOnce({ - access_token: "qw-acc", - refresh_token: "qw-refresh-rotated", - expires_in: 7200, - resource_url: "https://dashscope.aliyuncs.com", - }); - const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js"); - - const out = await refreshAccessToken("qwen", "qw-old-refresh", {}, console); - - expect(out).toEqual({ - accessToken: "qw-acc", - refreshToken: "qw-refresh-rotated", - expiresIn: 7200, - providerSpecificData: { resourceUrl: "https://dashscope.aliyuncs.com" }, - }); - const [url, init] = fm.mock.calls[0]; - expect(init.method).toBe("POST"); - expect(init.headers["Content-Type"]).toBe("application/x-www-form-urlencoded"); - const body = new URLSearchParams(init.body); - expect(body.get("grant_type")).toBe("refresh_token"); - expect(body.get("refresh_token")).toBe("qw-old-refresh"); - expect(body.get("client_id")).toBeTruthy(); - }); - it("iflow: Basic Auth header from clientId:clientSecret, form body keeps client_secret", async () => { const fm = mockFetchOnce({ access_token: "if-acc", refresh_token: "if-rot", expires_in: 3600 }); const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js"); @@ -107,13 +81,13 @@ describe("refreshAccessToken — config-driven profiles", () => { it("returns null on non-ok response", async () => { mockFetchOnce({ error: "invalid_grant" }, { ok: false, status: 400 }); const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js"); - const out = await refreshAccessToken("qwen", "dead", {}, console); + const out = await refreshAccessToken("iflow", "dead", {}, console); expect(out).toBeNull(); }); it("returns null when refreshToken missing", async () => { const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js"); - const out = await refreshAccessToken("qwen", "", {}, console); + const out = await refreshAccessToken("iflow", "", {}, console); expect(out).toBeNull(); }); diff --git a/tests/unit/usage-dispatch.test.js b/tests/unit/usage-dispatch.test.js index 21078e39..e0e80840 100644 --- a/tests/unit/usage-dispatch.test.js +++ b/tests/unit/usage-dispatch.test.js @@ -14,7 +14,7 @@ vi.mock("../../open-sse/utils/proxyFetch.js", () => ({ const load = () => import("../../open-sse/services/usage.js"); const SUPPORTED = [ "github", "gemini-cli", "antigravity", "claude", "codex", "kiro", - "qoder", "qwen", "iflow", "ollama", "glm", "glm-cn", + "qoder", "iflow", "ollama", "glm", "glm-cn", "minimax", "minimax-cn", "vercel-ai-gateway", "grok-cli", "kimi", "deepseek", ];