Files
9router/open-sse/executors/qwen.js

130 lines
4.7 KiB
JavaScript

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;