mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
130 lines
4.7 KiB
JavaScript
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;
|