UNPKG

koishi-plugin-chatluna-qwen-adapter

Version:
603 lines (593 loc) 20.8 kB
var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; // src/locales/zh-CN.schema.yml var require_zh_CN_schema = __commonJS({ "src/locales/zh-CN.schema.yml"(exports, module) { module.exports = { $inner: [{}, { $desc: "请求选项", apiKeys: { $inner: ["阿里云百炼的 API Key", "API 请求地址(可选)", "是否启用此配置"], $desc: "阿里云百炼的 API Key 列表。" }, additionalModels: { $desc: "额外模型列表。", $inner: { model: "模型名称", modelType: "模型类型", contextSize: "模型上下文大小", modelCapabilities: { $desc: "模型支持的能力", $inner: ["工具调用", "图片视觉输入"] } } } }, { $desc: "模型配置", maxContextRatio: "最大上下文使用比例(0~1),控制可用的模型上下文窗口大小的最大百分比。例如 0.35 表示最多使用模型上下文的 35%。", temperature: "回复的随机性程度,数值越高,回复越随机。", enableSearch: "是否启用模型自带夸克搜索功能。" }] }; } }); // src/locales/en-US.schema.yml var require_en_US_schema = __commonJS({ "src/locales/en-US.schema.yml"(exports, module) { module.exports = { $inner: [{}, { $desc: "API Configuration", apiKeys: { $inner: ["Tongyi Qianwen API Key", "API Endpoint (optional)", "Enabled"], $desc: "Tongyi Qianwen API Keys" }, additionalModels: { $desc: "Additional models", $inner: { model: "Model name", modelType: "Model type", contextSize: "Context size", modelCapabilities: { $desc: "Model supported capabilities", $inner: ["Tool calling", "Visual image input"] } } } }, { $desc: "Model Parameters", maxContextRatio: "Maximum context usage ratio (0-1). Controls the maximum percentage of model context window available for use. For example, 0.35 means at most 35% of the model context can be used.", temperature: "Sampling temperature (higher values increase randomness)", enableSearch: "Enable built-in Quark search" }] }; } }); // src/index.ts import { ChatLunaPlugin } from "koishi-plugin-chatluna/services/chat"; import { Schema } from "koishi"; // src/client.ts import { PlatformModelAndEmbeddingsClient } from "koishi-plugin-chatluna/llm-core/platform/client"; import { ChatLunaChatModel, ChatLunaEmbeddings } from "koishi-plugin-chatluna/llm-core/platform/model"; import { ModelCapabilities, ModelType } from "koishi-plugin-chatluna/llm-core/platform/types"; import { ChatLunaError as ChatLunaError2, ChatLunaErrorCode as ChatLunaErrorCode2 } from "koishi-plugin-chatluna/utils/error"; // src/requester.ts import { ChatGenerationChunk } from "@langchain/core/outputs"; import { ChatLunaError, ChatLunaErrorCode } from "koishi-plugin-chatluna/utils/error"; import { sseIterable } from "koishi-plugin-chatluna/utils/sse"; import { trackLogToLocal } from "koishi-plugin-chatluna/utils/logger"; import { ModelRequester } from "koishi-plugin-chatluna/llm-core/platform/api"; // src/utils.ts import { AIMessageChunk, ChatMessageChunk, FunctionMessageChunk, HumanMessageChunk, SystemMessageChunk, ToolMessageChunk } from "@langchain/core/messages"; import { zodToJsonSchema } from "zod-to-json-schema"; import { isMessageContentImageUrl } from "koishi-plugin-chatluna/utils/string"; import { fetchImageUrl, removeAdditionalProperties } from "@chatluna/v1-shared-adapter"; import { isZodSchemaV3 } from "@langchain/core/utils/types"; function formatToolsToQWenTools(tools) { if (tools.length < 1) { return void 0; } return tools.map(formatToolToQWenTool); } __name(formatToolsToQWenTools, "formatToolsToQWenTools"); function formatToolToQWenTool(tool) { const parameters = removeAdditionalProperties( isZodSchemaV3(tool.schema) ? zodToJsonSchema(tool.schema, { allowedAdditionalProperties: void 0 }) : tool.schema ); return { type: "function", function: { name: tool.name, description: tool.description, // any? // eslint-disable-next-line @typescript-eslint/no-explicit-any parameters } }; } __name(formatToolToQWenTool, "formatToolToQWenTool"); async function langchainMessageToQWenMessage(messages, plugin, model) { const result = []; for (const rawMessage of messages) { const role = messageTypeToQWenRole(rawMessage.getType()); const msg = { content: rawMessage.content || null, name: role === "assistant" || role === "tool" ? rawMessage.name : void 0, role, tool_call_id: rawMessage.tool_call_id }; if (rawMessage.getType() === "ai") { const toolCalls = rawMessage.tool_calls; if (Array.isArray(toolCalls) && toolCalls.length > 0) { msg.tool_calls = toolCalls.map((toolCall) => ({ id: toolCall.id, type: "function", function: { name: toolCall.name, arguments: JSON.stringify(toolCall.args) } })); } } if (msg.tool_calls == null) { delete msg.tool_calls; } if (msg.tool_call_id == null) { delete msg.tool_call_id; } if (msg.tool_calls) { for (const toolCall of msg.tool_calls) { const tool = toolCall.function; if (!tool.arguments) { continue; } tool.arguments = JSON.stringify(JSON.parse(tool.arguments)); } } const images = rawMessage.additional_kwargs.images; if ((model?.includes("qwen-vl") || model?.includes("omni") || model?.includes("qwen2.5-vl") || model?.includes("qwen2.5-omni") || model?.includes("qwen-omni") || model?.includes("qwen2-vl") || model?.includes("qvq")) && images != null) { msg.content = [ { type: "text", text: rawMessage.content } ]; for (const image of images) { msg.content.push({ type: "image_url", image_url: { url: image, detail: "low" } }); } } else if (Array.isArray(msg.content) && msg.content.length > 0) { msg.content = await Promise.all( msg.content.map(async (content) => { if (!isMessageContentImageUrl(content)) return content; try { const url = await fetchImageUrl(plugin, content); return { type: "image_url", image_url: { url, detail: "low" } }; } catch { return content; } }) ); } result.push(msg); } return result; } __name(langchainMessageToQWenMessage, "langchainMessageToQWenMessage"); function messageTypeToQWenRole(type) { switch (type) { case "system": return "system"; case "ai": return "assistant"; case "human": return "user"; case "function": return "function"; case "tool": return "tool"; default: throw new Error(`Unknown message type: ${type}`); } } __name(messageTypeToQWenRole, "messageTypeToQWenRole"); function convertDeltaToMessageChunk(delta, defaultRole) { const role = ((delta.role?.length ?? 0) > 0 ? delta.role : defaultRole).toLowerCase(); const content = delta.content ?? ""; const reasoningContent = delta.reasoning_content ?? ""; const additionalKwargs = {}; if (reasoningContent.length > 0) { additionalKwargs.reasoning_content = reasoningContent; } if (role === "user") { return new HumanMessageChunk({ content }); } else if (role === "assistant") { const toolCallChunks = []; if (Array.isArray(delta.tool_calls)) { for (const rawToolCall of delta.tool_calls) { toolCallChunks.push({ name: rawToolCall.function?.name, args: rawToolCall.function?.arguments, id: rawToolCall.id, index: rawToolCall.index }); } } return new AIMessageChunk({ content, tool_call_chunks: toolCallChunks, additional_kwargs: additionalKwargs }); } else if (role === "system") { return new SystemMessageChunk({ content }); } else if (role === "function") { return new FunctionMessageChunk({ content, additional_kwargs: additionalKwargs, name: delta.name }); } else if (role === "tool") { return new ToolMessageChunk({ content, additional_kwargs: additionalKwargs, tool_call_id: delta.tool_call_id }); } else { return new ChatMessageChunk({ content, role }); } } __name(convertDeltaToMessageChunk, "convertDeltaToMessageChunk"); // src/requester.ts import { createEmbeddings, createRequestContext } from "@chatluna/v1-shared-adapter"; import { AIMessageChunk as AIMessageChunk2 } from "@langchain/core/messages"; import { logger } from "koishi-plugin-chatluna"; var QWenRequester = class extends ModelRequester { constructor(ctx, _configPool, _pluginConfig, _plugin) { super(ctx, _configPool, _pluginConfig, _plugin); this._pluginConfig = _pluginConfig; } static { __name(this, "QWenRequester"); } async *completionStreamInternal(params) { let model = params.model; let enabledThinking = null; if (model.includes("thinking")) { enabledThinking = !model.includes("-non-thinking"); model = model.replace("-non-thinking", "").replace("-thinking", ""); } else if (model.includes("default")) { enabledThinking = true; model = model.replace("-default", "-thinking"); } const requestParams = { model, messages: await langchainMessageToQWenMessage( params.input, this._plugin, model ), tools: params.tools != null && !params.model.includes("vl") ? formatToolsToQWenTools(params.tools) : void 0, stream: true, top_p: params.topP, temperature: params.temperature, enable_search: params.model.includes("vl") ? void 0 : this._pluginConfig.enableSearch, enabled_thinking: enabledThinking }; try { const response = await this.post( "chat/completions", requestParams, { signal: params.signal } ); let iterator; try { iterator = sseIterable(response); } catch (e) { if (e instanceof ChatLunaError && e.message.includes("data_inspection_failed")) { throw new ChatLunaError( ChatLunaErrorCode.API_UNSAFE_CONTENT, e ); } throw e; } const defaultRole = "assistant"; let reasoningContent = ""; let isSetReasoningTime = false; let reasoningTime = 0; for await (const event of iterator) { const chunk = event.data; if (chunk === "[DONE]") { return; } let data; try { data = JSON.parse(chunk); } catch (err) { throw new ChatLunaError( ChatLunaErrorCode.API_REQUEST_FAILED, new Error( "error when calling qwen completion, Result: " + chunk ) ); } const choice = data.choices?.[0]; if (data.usage) { yield new ChatGenerationChunk({ message: new AIMessageChunk2({ content: "", response_metadata: { tokenUsage: { promptTokens: data.usage.prompt_tokens, completionTokens: data.usage.completion_tokens, totalTokens: data.usage.total_tokens } } }), text: "" }); } if (!choice) { continue; } const delta = choice.delta; const messageChunk = convertDeltaToMessageChunk( delta, defaultRole ); if (delta.reasoning_content) { reasoningContent = reasoningContent + delta.reasoning_content; if (reasoningTime === 0) { reasoningTime = Date.now(); } } if ((delta.reasoning_content == null || delta.reasoning_content === "") && delta.content && delta.content.length > 0 && reasoningTime > 0 && !isSetReasoningTime) { reasoningTime = Date.now() - reasoningTime; messageChunk.additional_kwargs.reasoning_time = reasoningTime; isSetReasoningTime = true; } const generationChunk = new ChatGenerationChunk({ message: messageChunk, text: messageChunk.content }); yield generationChunk; if (choice.finish_reason === "stop") { break; } } if (reasoningContent.length > 0) { logger.debug( "reasoningContent: " + reasoningContent + ", reasoningTime: " + reasoningTime / 1e3 + "s" ); } } catch (e) { if (this.ctx.chatluna.config.isLog) { await trackLogToLocal( "Request", JSON.stringify(requestParams), this.logger ); } if (e instanceof ChatLunaError) { throw e; } else { throw new ChatLunaError(ChatLunaErrorCode.API_REQUEST_FAILED, e); } } } async embeddings(params) { const requestContext = createRequestContext( this.ctx, this._config.value, this._pluginConfig, this._plugin, this ); return await createEmbeddings(requestContext, params); } concatUrl(url) { return "https://dashscope.aliyuncs.com/compatible-mode/v1/" + url; } get logger() { return this.ctx.logger("chatluna-qwen-adapter"); } }; // src/client.ts import { supportImageInput } from "@chatluna/v1-shared-adapter"; var QWenClient = class extends PlatformModelAndEmbeddingsClient { constructor(ctx, _config, plugin) { super(ctx, plugin.platformConfigPool); this._config = _config; this.plugin = plugin; this._requester = new QWenRequester( ctx, plugin.platformConfigPool, _config, plugin ); } static { __name(this, "QWenClient"); } platform = "qwen"; _requester; async refreshModels() { const rawModels = [ ["qwen-turbo", 1e5], ["qwen-long", 1e6], ["qwen-plus", 131072], ["qwen-plus-character", 32768], ["qwen-max", 30720], ["qwen-max-latest", 131072], ["qwen3-max", 262144], ["qwen-plus-latest-non-thinking", 1e6], ["qwen-plus-latest-thinking", 1e6], ["qwen-turbo-latest-non-thinking", 1e6], ["qwen-turbo-latest-thinking", 1e6], ["qwen-flash", 1e6], ["qwen3-vl-plus-thinking", 262144], ["qwen3-vl-plus-non-thinking", 262144], ["qwen-vl-max", 131072], ["qwen-vl-max-latest", 131072], ["qwen-vl-plus", 131072], ["qwen-vl-plus-latest", 131072], ["qwen-vl-ocr", 34096], ["qwen-vl-ocr-latest", 34096], ["qwq-32b-preview", 30720], ["qvq-72b-preview", 30720], ["qwq-plus", 131072], ["qwq-plus-latest", 131072], ["qwen-omni-turbo", 32768], ["qwen-omni-turbo-latest", 32768], ["qwen-math-plus", 4e3], ["qwen-math-turbo", 4e3], ["qwen3-next-80b-a3b-default", 126976], ["qwen3-next-80b-a3b-instruct", 126024], ["qwen3-235b-a22b-default-2507", 131072], ["qwen3-235b-a22b-instruct-2507", 131072], ["qwen3-32b-thinking", 131072], ["qwen3-32b-non-thinking", 131072], ["qwen3-30b-a3b-thinking", 131072], ["qwen3-30b-a3b-non-thinking", 131072], ["qwen3-14b-thinking", 131072], ["qwen3-14b-non-thinking", 131072], ["qwen3-8b-thinking", 131072], ["qwen3-8b-non-thinking", 131072], ["qwen3-4b-thinking", 131072], ["qwen3-4b-non-thinking", 131072], ["qwen3-1.7b-thinking", 30720], ["qwen3-1.7b-non-thinking", 30720], ["qwen3-0.6b-thinking", 30720], ["qwen3-0.6b-non-thinking", 30720], ["qwen3-omni-flash-thinking", 65536], ["qwen3-omni-flash-non-thinking", 65536], ["qwen-omni-turbo", 32768], ["qwen-omni-latest", 32768], ["qwen3-vl-235b-a22b-default", 131072], ["qwen3-vl-235b-a22b-instruct", 131072], ["qwen2.5-vl-72b-instruct", 131072], ["qwen2.5-vl-32b-instruct", 129024], ["qwen2.5-vl-7b-instruct", 8192], ["qwen2.5-vl-3b-instruct", 8192], ["qwen-vl-v1", 8e3], ["Moonshot-Kimi-K2-Instruct", 131072], ["deepseek-r1", 131072], ["deepseek-v3", 65536], ["text-embedding-v1", 2048], ["text-embedding-v2", 2048], ["text-embedding-v3", 8192] ]; const additionalModels = this._config.additionalModels.map( ({ model, modelType, contextSize, modelCapabilities }) => ({ name: model, type: modelType === "Embeddings 嵌入模型" ? ModelType.embeddings : ModelType.llm, capabilities: modelCapabilities, maxTokens: contextSize ?? 4096 }) ); return rawModels.map(([model, token]) => { return { name: model, type: model.includes("embedding") ? ModelType.embeddings : ModelType.llm, maxTokens: token, capabilities: [ (model.includes("qwen-plus") || model.includes("qwen-max") || model.includes("qwen-turbo") || model.includes("qwen3") || model.includes("qwen2.5") || model.includes("omni") || model.includes("Kimi-K2") || model.includes("deepseek")) && ModelCapabilities.ToolCall, supportImageInput(model) && ModelCapabilities.ImageInput ].filter(Boolean) }; }).concat(additionalModels); } _createModel(model) { const info = this._modelInfos[model]; if (info == null) { throw new ChatLunaError2(ChatLunaErrorCode2.MODEL_NOT_FOUND); } if (info.type === ModelType.llm) { const modelMaxContextSize = info.maxTokens; return new ChatLunaChatModel({ modelInfo: info, requester: this._requester, model, modelMaxContextSize, maxTokenLimit: Math.floor( (info.maxTokens || modelMaxContextSize || 128e3) * this._config.maxContextRatio ), timeout: this._config.timeout, temperature: this._config.temperature, maxRetries: this._config.maxRetries, llmType: "qwen", isThinkModel: model.includes("reasoner") || model.includes("r1") || model.includes("thinking") || model.includes("qwq") }); } return new ChatLunaEmbeddings({ client: this._requester, model: info.name, batchSize: 5, maxRetries: this._config.maxRetries }); } }; // src/index.ts import { ModelCapabilities as ModelCapabilities2 } from "koishi-plugin-chatluna/llm-core/platform/types"; function apply(ctx, config) { ctx.on("ready", async () => { const plugin = new ChatLunaPlugin(ctx, config, "qwen"); plugin.parseConfig((config2) => { return config2.apiKeys.filter(([apiKey, enabled]) => { return apiKey.length > 0 && enabled; }).map(([apiKey]) => { return { apiKey, apiEndpoint: "", platform: "qwen", chatLimit: config2.chatTimeLimit, timeout: config2.timeout, maxRetries: config2.maxRetries, concurrentMaxSize: config2.chatConcurrentMaxSize }; }); }); plugin.registerClient(() => new QWenClient(ctx, config, plugin)); await plugin.initClient(); }); } __name(apply, "apply"); var Config = Schema.intersect([ ChatLunaPlugin.Config, Schema.object({ apiKeys: Schema.array( Schema.tuple([ Schema.string().role("secret").default(""), Schema.boolean().default(true) ]) ).default([[]]).role("table"), additionalModels: Schema.array( Schema.object({ model: Schema.string(), modelType: Schema.union([ "LLM 大语言模型", "Embeddings 嵌入模型" ]).default("LLM 大语言模型"), modelCapabilities: Schema.array( Schema.union([ ModelCapabilities2.ToolCall, ModelCapabilities2.ImageInput ]) ).default([ModelCapabilities2.ToolCall]).role("checkbox"), contextSize: Schema.number().default(128e3) }) ).default([]).role("table") }), Schema.object({ maxContextRatio: Schema.number().min(0).max(1).step(1e-4).role("slider").default(0.35), temperature: Schema.percent().min(0).max(2).step(0.1).default(1), enableSearch: Schema.boolean().default(true) }) ]).i18n({ "zh-CN": require_zh_CN_schema(), "en-US": require_en_US_schema() // eslint-disable-next-line @typescript-eslint/no-explicit-any }); var inject = ["chatluna"]; var name = "chatluna-qwen-adapter"; export { Config, apply, inject, name };