koishi-plugin-chatluna-google-gemini-adapter 1.0.0-beta.8 → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.mjs ADDED
@@ -0,0 +1,677 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+ var __commonJS = (cb, mod) => function __require() {
5
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
6
+ };
7
+
8
+ // src/locales/zh-CN.schema.yml
9
+ var require_zh_CN_schema = __commonJS({
10
+ "src/locales/zh-CN.schema.yml"(exports, module) {
11
+ module.exports = { inner: [{}, { $desc: "请求选项", apiKeys: { $inner: ["Gemini 的 API Key", "Gemini API 的请求地址"], $desc: "Gemini 的 API Key 和请求地址列表。" } }, { $desc: "模型配置", maxTokens: "回复的最大 Token 数(16~2097000,必须是 16 的倍数)。注意:仅当您使用的模型最大 Token 为 8000 及以上时,才建议设置超过 2000 token。", temperature: "回复的随机性程度,数值越高,回复越随机(范围:0~2)。" }] };
12
+ }
13
+ });
14
+
15
+ // src/locales/en-US.schema.yml
16
+ var require_en_US_schema = __commonJS({
17
+ "src/locales/en-US.schema.yml"(exports, module) {
18
+ module.exports = { $inner: [{}, { $desc: "API Configuration", apiKeys: { $inner: ["Gemini API Key", "Gemini API Endpoint (optional)"], $desc: "Gemini API access credentials" } }, { $desc: "Model Parameters", maxTokens: "Max output tokens (16-2097000, multiple of 16). >2000 for 8k+ models", temperature: "Sampling temperature (0-2). Higher: more random, Lower: more deterministic" }] };
19
+ }
20
+ });
21
+
22
+ // src/index.ts
23
+ import { ChatLunaPlugin } from "koishi-plugin-chatluna/services/chat";
24
+ import { Schema } from "koishi";
25
+
26
+ // src/client.ts
27
+ import { PlatformModelAndEmbeddingsClient } from "koishi-plugin-chatluna/llm-core/platform/client";
28
+ import {
29
+ ChatLunaChatModel,
30
+ ChatLunaEmbeddings
31
+ } from "koishi-plugin-chatluna/llm-core/platform/model";
32
+ import {
33
+ ModelType
34
+ } from "koishi-plugin-chatluna/llm-core/platform/types";
35
+ import {
36
+ ChatLunaError as ChatLunaError2,
37
+ ChatLunaErrorCode as ChatLunaErrorCode2
38
+ } from "koishi-plugin-chatluna/utils/error";
39
+
40
+ // src/requester.ts
41
+ import { AIMessageChunk as AIMessageChunk2 } from "@langchain/core/messages";
42
+ import { ChatGenerationChunk } from "@langchain/core/outputs";
43
+ import { JSONParser } from "@streamparser/json";
44
+ import {
45
+ ModelRequester
46
+ } from "koishi-plugin-chatluna/llm-core/platform/api";
47
+ import {
48
+ ChatLunaError,
49
+ ChatLunaErrorCode
50
+ } from "koishi-plugin-chatluna/utils/error";
51
+ import { sse } from "koishi-plugin-chatluna/utils/sse";
52
+ import { readableStreamToAsyncIterable } from "koishi-plugin-chatluna/utils/stream";
53
+
54
+ // src/utils.ts
55
+ import {
56
+ AIMessageChunk,
57
+ ChatMessageChunk,
58
+ HumanMessageChunk,
59
+ SystemMessageChunk
60
+ } from "@langchain/core/messages";
61
+ import { zodToJsonSchema } from "zod-to-json-schema";
62
+ async function langchainMessageToGeminiMessage(messages, model) {
63
+ const mappedMessage = await Promise.all(
64
+ messages.map(async (rawMessage) => {
65
+ const role = messageTypeToGeminiRole(rawMessage._getType());
66
+ if (role === "function" || rawMessage.additional_kwargs?.function_call != null) {
67
+ return {
68
+ role: "function",
69
+ parts: [
70
+ {
71
+ functionResponse: rawMessage.additional_kwargs?.function_call != null ? void 0 : {
72
+ name: rawMessage.name,
73
+ response: {
74
+ name: rawMessage.name,
75
+ content: (() => {
76
+ try {
77
+ const result3 = JSON.parse(
78
+ rawMessage.content
79
+ );
80
+ if (typeof result3 === "string") {
81
+ return {
82
+ response: result3
83
+ };
84
+ } else {
85
+ return result3;
86
+ }
87
+ } catch (e) {
88
+ return {
89
+ response: rawMessage.content
90
+ };
91
+ }
92
+ })()
93
+ }
94
+ },
95
+ functionCall: rawMessage.additional_kwargs?.function_call != null ? {
96
+ name: rawMessage.additional_kwargs.function_call.name,
97
+ args: (() => {
98
+ try {
99
+ const result3 = JSON.parse(
100
+ rawMessage.additional_kwargs.function_call.arguments
101
+ );
102
+ if (typeof result3 === "string") {
103
+ return {
104
+ input: result3
105
+ };
106
+ } else {
107
+ return result3;
108
+ }
109
+ } catch (e) {
110
+ return {
111
+ input: rawMessage.additional_kwargs.function_call.arguments
112
+ };
113
+ }
114
+ })()
115
+ } : void 0
116
+ }
117
+ ]
118
+ };
119
+ }
120
+ const images = rawMessage.additional_kwargs.images;
121
+ const result2 = {
122
+ role,
123
+ parts: [
124
+ {
125
+ text: rawMessage.content
126
+ }
127
+ ]
128
+ };
129
+ if ((model.includes("vision") || model.includes("gemini-1.5")) && images != null) {
130
+ for (const image of images) {
131
+ result2.parts.push({
132
+ inline_data: {
133
+ // base64 image match type
134
+ data: image.replace(/^data:image\/\w+;base64,/, ""),
135
+ mime_type: "image/jpeg"
136
+ }
137
+ });
138
+ }
139
+ }
140
+ return result2;
141
+ })
142
+ );
143
+ const result = [];
144
+ for (let i = 0; i < mappedMessage.length; i++) {
145
+ const message = mappedMessage[i];
146
+ if (message.role !== "system") {
147
+ result.push(message);
148
+ continue;
149
+ }
150
+ result.push({
151
+ role: "user",
152
+ parts: message.parts
153
+ });
154
+ const nextMessage = mappedMessage?.[i + 1];
155
+ if (nextMessage?.role === "model") {
156
+ continue;
157
+ }
158
+ if (nextMessage?.role === "user" || nextMessage?.role === "system") {
159
+ result.push({
160
+ role: "model",
161
+ parts: [{ text: "Okay, what do I need to do?" }]
162
+ });
163
+ }
164
+ if (nextMessage?.role === "system") {
165
+ result.push({
166
+ role: "user",
167
+ parts: [
168
+ {
169
+ text: "Continue what I said to you last message. Follow these instructions."
170
+ }
171
+ ]
172
+ });
173
+ }
174
+ }
175
+ if (result[result.length - 1].role === "model") {
176
+ result.push({
177
+ role: "user",
178
+ parts: [
179
+ {
180
+ text: "Continue what I said to you last message. Follow these instructions."
181
+ }
182
+ ]
183
+ });
184
+ }
185
+ if (model.includes("vision")) {
186
+ const textBuffer = [];
187
+ const last = result.pop();
188
+ for (let i = 0; i < result.length; i++) {
189
+ const message = result[i];
190
+ const text = message.parts[0].text;
191
+ textBuffer.push(`${message.role}: ${text}`);
192
+ }
193
+ const lastParts = last.parts;
194
+ let lastImagesParts = lastParts.filter(
195
+ (part) => part.inline_data?.mime_type === "image/jpeg"
196
+ );
197
+ if (lastImagesParts.length < 1) {
198
+ for (let i = result.length - 1; i >= 0; i--) {
199
+ const message = result[i];
200
+ const images = message.parts.filter(
201
+ (part) => part.inline_data?.mime_type === "image/jpeg"
202
+ );
203
+ if (images.length > 0) {
204
+ lastImagesParts = images;
205
+ break;
206
+ }
207
+ }
208
+ }
209
+ ;
210
+ lastParts.filter(
211
+ (part) => part.text !== void 0 && part.text !== null
212
+ ).forEach((part) => {
213
+ textBuffer.push(`${last.role}: ${part.text}`);
214
+ });
215
+ return [
216
+ {
217
+ role: "user",
218
+ parts: [
219
+ {
220
+ text: textBuffer.join("\n")
221
+ },
222
+ ...lastImagesParts
223
+ ]
224
+ }
225
+ ];
226
+ }
227
+ return result;
228
+ }
229
+ __name(langchainMessageToGeminiMessage, "langchainMessageToGeminiMessage");
230
+ function partAsType(part) {
231
+ return part;
232
+ }
233
+ __name(partAsType, "partAsType");
234
+ function formatToolsToGeminiAITools(tools) {
235
+ if (tools.length < 1) {
236
+ return void 0;
237
+ }
238
+ return tools.map(formatToolToGeminiAITool);
239
+ }
240
+ __name(formatToolsToGeminiAITools, "formatToolsToGeminiAITools");
241
+ function formatToolToGeminiAITool(tool) {
242
+ const parameters = zodToJsonSchema(tool.schema);
243
+ delete parameters["$schema"];
244
+ delete parameters["additionalProperties"];
245
+ return {
246
+ name: tool.name,
247
+ description: tool.description,
248
+ // any?
249
+ parameters
250
+ };
251
+ }
252
+ __name(formatToolToGeminiAITool, "formatToolToGeminiAITool");
253
+ function messageTypeToGeminiRole(type) {
254
+ switch (type) {
255
+ case "system":
256
+ return "system";
257
+ case "ai":
258
+ return "model";
259
+ case "human":
260
+ return "user";
261
+ case "function":
262
+ return "function";
263
+ default:
264
+ throw new Error(`Unknown message type: ${type}`);
265
+ }
266
+ }
267
+ __name(messageTypeToGeminiRole, "messageTypeToGeminiRole");
268
+
269
+ // src/requester.ts
270
+ var GeminiRequester = class extends ModelRequester {
271
+ constructor(_config, _plugin) {
272
+ super();
273
+ this._config = _config;
274
+ this._plugin = _plugin;
275
+ }
276
+ static {
277
+ __name(this, "GeminiRequester");
278
+ }
279
+ async *completionStream(params) {
280
+ try {
281
+ const response = await this._post(
282
+ `models/${params.model}:streamGenerateContent`,
283
+ {
284
+ contents: await langchainMessageToGeminiMessage(
285
+ params.input,
286
+ params.model
287
+ ),
288
+ safetySettings: [
289
+ {
290
+ category: "HARM_CATEGORY_HARASSMENT",
291
+ threshold: "BLOCK_NONE"
292
+ },
293
+ {
294
+ category: "HARM_CATEGORY_HATE_SPEECH",
295
+ threshold: "BLOCK_NONE"
296
+ },
297
+ {
298
+ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
299
+ threshold: "BLOCK_NONE"
300
+ },
301
+ {
302
+ category: "HARM_CATEGORY_DANGEROUS_CONTENT",
303
+ threshold: "BLOCK_NONE"
304
+ }
305
+ ],
306
+ generationConfig: {
307
+ stopSequences: params.stop,
308
+ temperature: params.temperature,
309
+ maxOutputTokens: params.model.includes("vision") ? void 0 : params.maxTokens,
310
+ topP: params.topP
311
+ },
312
+ tools: !params.model.includes("vision") && params.tools != null ? {
313
+ functionDeclarations: formatToolsToGeminiAITools(params.tools)
314
+ } : void 0
315
+ },
316
+ {
317
+ signal: params.signal
318
+ }
319
+ );
320
+ let errorCount = 0;
321
+ const stream = new TransformStream();
322
+ const iterable = readableStreamToAsyncIterable(
323
+ stream.readable
324
+ );
325
+ const jsonParser = new JSONParser();
326
+ const writable = stream.writable.getWriter();
327
+ jsonParser.onEnd = async () => {
328
+ await writable.close();
329
+ };
330
+ jsonParser.onValue = async ({ value }) => {
331
+ const transformValue = value;
332
+ if (transformValue.candidates && transformValue.candidates[0]) {
333
+ const parts = transformValue.candidates[0]?.content?.parts;
334
+ if (parts == null || parts.length < 1) {
335
+ throw new Error(JSON.stringify(value));
336
+ }
337
+ for (const part of parts) {
338
+ await writable.write(part);
339
+ }
340
+ }
341
+ };
342
+ await sse(
343
+ response,
344
+ async (rawData) => {
345
+ jsonParser.write(rawData);
346
+ return true;
347
+ },
348
+ 0
349
+ );
350
+ let content = "";
351
+ let isOldVisionModel = params.model.includes("vision");
352
+ const functionCall = {
353
+ name: "",
354
+ args: "",
355
+ arguments: ""
356
+ };
357
+ for await (const chunk of iterable) {
358
+ const messagePart = partAsType(chunk);
359
+ const chatFunctionCallingPart = partAsType(chunk);
360
+ if (messagePart.text) {
361
+ if (params.tools != null) {
362
+ content = messagePart.text;
363
+ } else {
364
+ content += messagePart.text;
365
+ }
366
+ if (isOldVisionModel && /\s*model:\s*/.test(content)) {
367
+ isOldVisionModel = false;
368
+ content = messagePart.text.replace(/\s*model:\s*/, "");
369
+ }
370
+ }
371
+ const deltaFunctionCall = chatFunctionCallingPart.functionCall;
372
+ if (deltaFunctionCall) {
373
+ let args = deltaFunctionCall.args?.input ?? deltaFunctionCall.args;
374
+ try {
375
+ let parsedArgs = JSON.parse(args);
376
+ if (typeof parsedArgs !== "string") {
377
+ args = parsedArgs;
378
+ }
379
+ parsedArgs = JSON.parse(args);
380
+ if (typeof parsedArgs !== "string") {
381
+ args = parsedArgs;
382
+ }
383
+ } catch (e) {
384
+ }
385
+ functionCall.args = JSON.stringify(args);
386
+ functionCall.name = deltaFunctionCall.name;
387
+ functionCall.arguments = deltaFunctionCall.args;
388
+ }
389
+ try {
390
+ const messageChunk = new AIMessageChunk2(content);
391
+ messageChunk.additional_kwargs = {
392
+ function_call: functionCall.name.length > 0 ? {
393
+ name: functionCall.name,
394
+ arguments: functionCall.args,
395
+ args: functionCall.arguments
396
+ } : void 0
397
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
398
+ };
399
+ messageChunk.content = content;
400
+ const generationChunk = new ChatGenerationChunk({
401
+ message: messageChunk,
402
+ text: messageChunk.content
403
+ });
404
+ yield generationChunk;
405
+ content = messageChunk.content;
406
+ } catch (e) {
407
+ if (errorCount > 5) {
408
+ logger.error("error with chunk", chunk);
409
+ throw new ChatLunaError(
410
+ ChatLunaErrorCode.API_REQUEST_FAILED,
411
+ e
412
+ );
413
+ } else {
414
+ errorCount++;
415
+ continue;
416
+ }
417
+ }
418
+ }
419
+ } catch (e) {
420
+ if (e instanceof ChatLunaError) {
421
+ throw e;
422
+ } else {
423
+ throw new ChatLunaError(ChatLunaErrorCode.API_REQUEST_FAILED, e);
424
+ }
425
+ }
426
+ }
427
+ async embeddings(params) {
428
+ let data;
429
+ if (typeof params.input === "string") {
430
+ params.input = [params.input];
431
+ }
432
+ try {
433
+ const response = await this._post(
434
+ `models/${params.model}:batchEmbedContents`,
435
+ {
436
+ requests: params.input.map((input) => {
437
+ return {
438
+ model: `models/${params.model}`,
439
+ content: {
440
+ parts: [
441
+ {
442
+ text: input
443
+ }
444
+ ]
445
+ }
446
+ };
447
+ })
448
+ }
449
+ );
450
+ data = await response.text();
451
+ data = JSON.parse(data);
452
+ if (data.embeddings && data.embeddings.length > 0) {
453
+ return data.embeddings.map((embedding) => {
454
+ return embedding.values;
455
+ });
456
+ }
457
+ throw new Error(
458
+ "error when calling gemini embeddings, Result: " + JSON.stringify(data)
459
+ );
460
+ } catch (e) {
461
+ const error = new Error(
462
+ "error when calling gemini embeddings, Result: " + JSON.stringify(data)
463
+ );
464
+ error.stack = e.stack;
465
+ error.cause = e.cause;
466
+ logger.debug(e);
467
+ throw new ChatLunaError(ChatLunaErrorCode.API_REQUEST_FAILED, error);
468
+ }
469
+ }
470
+ async getModels() {
471
+ let data;
472
+ try {
473
+ const response = await this._get("models");
474
+ data = await response.text();
475
+ data = JSON.parse(data);
476
+ if (!data.models || !data.models.length) {
477
+ throw new Error(
478
+ "error when listing gemini models, Result:" + JSON.stringify(data)
479
+ );
480
+ }
481
+ return data.models.map((model) => model.name).filter(
482
+ (model) => model.includes("gemini") || model.includes("embedding")
483
+ );
484
+ } catch (e) {
485
+ const error = new Error(
486
+ "error when listing gemini models, Result: " + JSON.stringify(data)
487
+ );
488
+ error.stack = e.stack;
489
+ error.cause = e.cause;
490
+ throw error;
491
+ }
492
+ }
493
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
494
+ _post(url, data, params = {}) {
495
+ const requestUrl = this._concatUrl(url);
496
+ for (const key in data) {
497
+ if (data[key] === void 0) {
498
+ delete data[key];
499
+ }
500
+ }
501
+ const body = JSON.stringify(data);
502
+ return this._plugin.fetch(requestUrl, {
503
+ body,
504
+ headers: this._buildHeaders(),
505
+ method: "POST",
506
+ ...params
507
+ });
508
+ }
509
+ _get(url) {
510
+ const requestUrl = this._concatUrl(url);
511
+ return this._plugin.fetch(requestUrl, {
512
+ method: "GET",
513
+ headers: this._buildHeaders()
514
+ });
515
+ }
516
+ _concatUrl(url) {
517
+ const apiEndPoint = this._config.apiEndpoint;
518
+ if (apiEndPoint.endsWith("/")) {
519
+ return apiEndPoint + url + `?key=${this._config.apiKey}`;
520
+ }
521
+ return apiEndPoint + "/" + url + `?key=${this._config.apiKey}`;
522
+ }
523
+ _buildHeaders() {
524
+ return {
525
+ /* Authorization: `Bearer ${this._config.apiKey}`, */
526
+ "Content-Type": "application/json"
527
+ };
528
+ }
529
+ async init() {
530
+ }
531
+ async dispose() {
532
+ }
533
+ };
534
+
535
+ // src/client.ts
536
+ var GeminiClient = class extends PlatformModelAndEmbeddingsClient {
537
+ constructor(ctx, _config, clientConfig, plugin) {
538
+ super(ctx, clientConfig);
539
+ this._config = _config;
540
+ this._requester = new GeminiRequester(clientConfig, plugin);
541
+ }
542
+ static {
543
+ __name(this, "GeminiClient");
544
+ }
545
+ platform = "gemini";
546
+ _requester;
547
+ _models;
548
+ async init() {
549
+ await this.getModels();
550
+ }
551
+ async refreshModels() {
552
+ try {
553
+ const rawModels = await this._requester.getModels();
554
+ if (!rawModels.length) {
555
+ throw new ChatLunaError2(
556
+ ChatLunaErrorCode2.MODEL_INIT_ERROR,
557
+ new Error("No model found")
558
+ );
559
+ }
560
+ return rawModels.map((model) => model.replace("models/", "")).map((model) => {
561
+ return {
562
+ name: model,
563
+ maxTokens: ((model2) => {
564
+ if (model2.includes("gemini-1.5-pro")) {
565
+ return 1048576;
566
+ }
567
+ if (model2.includes("gemini-1.5-flash")) {
568
+ return 2097152;
569
+ }
570
+ if (model2.includes("gemini-1.0-pro")) {
571
+ return 30720;
572
+ }
573
+ return 30720;
574
+ })(model),
575
+ type: model.includes("embedding") ? ModelType.embeddings : ModelType.llm,
576
+ functionCall: !model.includes("vision"),
577
+ supportMode: ["all"]
578
+ };
579
+ });
580
+ } catch (e) {
581
+ throw new ChatLunaError2(ChatLunaErrorCode2.MODEL_INIT_ERROR, e);
582
+ }
583
+ }
584
+ async getModels() {
585
+ if (this._models) {
586
+ return Object.values(this._models);
587
+ }
588
+ const models = await this.refreshModels();
589
+ this._models = {};
590
+ for (const model of models) {
591
+ this._models[model.name] = model;
592
+ }
593
+ }
594
+ _createModel(model) {
595
+ const info = this._models[model];
596
+ if (info == null) {
597
+ throw new ChatLunaError2(ChatLunaErrorCode2.MODEL_NOT_FOUND);
598
+ }
599
+ if (info.type === ModelType.llm) {
600
+ return new ChatLunaChatModel({
601
+ modelInfo: info,
602
+ requester: this._requester,
603
+ model,
604
+ modelMaxContextSize: info.maxTokens,
605
+ maxTokenLimit: this._config.maxTokens,
606
+ timeout: this._config.timeout,
607
+ temperature: this._config.temperature,
608
+ maxRetries: this._config.maxRetries,
609
+ llmType: "gemini"
610
+ });
611
+ }
612
+ return new ChatLunaEmbeddings({
613
+ client: this._requester,
614
+ model,
615
+ maxRetries: this._config.maxRetries
616
+ });
617
+ }
618
+ };
619
+
620
+ // src/index.ts
621
+ import { createLogger } from "koishi-plugin-chatluna/utils/logger";
622
+ var logger;
623
+ function apply(ctx, config) {
624
+ const plugin = new ChatLunaPlugin(ctx, config, "gemini");
625
+ logger = createLogger(ctx, "chatluna-gemini-adapter");
626
+ ctx.on("ready", async () => {
627
+ plugin.registerToService();
628
+ await plugin.parseConfig((config2) => {
629
+ return config2.apiKeys.map(([apiKey, apiEndpoint]) => {
630
+ return {
631
+ apiKey,
632
+ apiEndpoint,
633
+ platform: "gemini",
634
+ chatLimit: config2.chatTimeLimit,
635
+ timeout: config2.timeout,
636
+ maxRetries: config2.maxRetries,
637
+ concurrentMaxSize: config2.chatConcurrentMaxSize
638
+ };
639
+ });
640
+ });
641
+ plugin.registerClient(
642
+ (_, clientConfig) => new GeminiClient(ctx, config, clientConfig, plugin)
643
+ );
644
+ await plugin.initClients();
645
+ });
646
+ }
647
+ __name(apply, "apply");
648
+ var Config = Schema.intersect([
649
+ ChatLunaPlugin.Config,
650
+ Schema.object({
651
+ apiKeys: Schema.array(
652
+ Schema.tuple([
653
+ Schema.string().role("secret"),
654
+ Schema.string().default(
655
+ "https://generativelanguage.googleapis.com/v1beta"
656
+ )
657
+ ])
658
+ ).default([["", "https://generativelanguage.googleapis.com/v1beta"]])
659
+ }),
660
+ Schema.object({
661
+ maxTokens: Schema.number().min(16).max(2097e3).step(16).default(8064),
662
+ temperature: Schema.percent().min(0).max(2).step(0.1).default(0.8)
663
+ })
664
+ ]).i18n({
665
+ "zh-CN": require_zh_CN_schema(),
666
+ "en-US": require_en_US_schema()
667
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
668
+ });
669
+ var inject = ["chatluna"];
670
+ var name = "chatluna-google-gemini-adapter";
671
+ export {
672
+ Config,
673
+ apply,
674
+ inject,
675
+ logger,
676
+ name
677
+ };