@chatluna/v1-shared-adapter 1.0.27 → 1.0.29

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.cjs CHANGED
@@ -27,7 +27,9 @@ __export(index_exports, {
27
27
  convertMessageToMessageChunk: () => convertMessageToMessageChunk,
28
28
  createEmbeddings: () => createEmbeddings,
29
29
  createRequestContext: () => createRequestContext,
30
+ createUsageMetadata: () => createUsageMetadata,
30
31
  expandReasoningEffortModelVariants: () => expandReasoningEffortModelVariants,
32
+ fetchFileLikeUrl: () => fetchFileLikeUrl,
31
33
  fetchImageUrl: () => fetchImageUrl,
32
34
  formatToolToOpenAITool: () => formatToolToOpenAITool,
33
35
  formatToolsToOpenAITools: () => formatToolsToOpenAITools,
@@ -38,9 +40,9 @@ __export(index_exports, {
38
40
  langchainMessageToOpenAIMessage: () => langchainMessageToOpenAIMessage,
39
41
  messageTypeToOpenAIRole: () => messageTypeToOpenAIRole,
40
42
  normalizeOpenAIModelName: () => normalizeOpenAIModelName,
43
+ openAIUsageToUsageMetadata: () => openAIUsageToUsageMetadata,
41
44
  parseOpenAIModelNameWithReasoningEffort: () => parseOpenAIModelNameWithReasoningEffort,
42
45
  processDeepSeekThinkMessages: () => processDeepSeekThinkMessages,
43
- processReasoningContent: () => processReasoningContent,
44
46
  processResponse: () => processResponse,
45
47
  processStreamResponse: () => processStreamResponse,
46
48
  reasoningEffortModelSuffixes: () => reasoningEffortModelSuffixes,
@@ -163,6 +165,8 @@ var imageModelMatchers = [
163
165
  "qwen*-omni",
164
166
  "qwen-omni",
165
167
  "qwen*-vl",
168
+ "qwen-3.5",
169
+ "qwen3.5",
166
170
  "qvq",
167
171
  "o1",
168
172
  "o3",
@@ -190,7 +194,38 @@ var import_messages = require("@langchain/core/messages");
190
194
  var import_zod_to_json_schema = require("zod-to-json-schema");
191
195
  var import_string = require("koishi-plugin-chatluna/utils/string");
192
196
  var import_types = require("@langchain/core/utils/types");
193
- async function langchainMessageToOpenAIMessage(messages, plugin, model, supportImageInput2, removeSystemMessage) {
197
+ function createUsageMetadata(data) {
198
+ const inputTokenDetails = {
199
+ ...data.inputAudioTokens != null ? { audio: data.inputAudioTokens } : {},
200
+ ...data.cacheReadTokens != null ? { cache_read: data.cacheReadTokens } : {},
201
+ ...data.cacheCreationTokens != null ? { cache_creation: data.cacheCreationTokens } : {}
202
+ };
203
+ const outputTokenDetails = {
204
+ ...data.outputAudioTokens != null ? { audio: data.outputAudioTokens } : {},
205
+ ...data.reasoningTokens != null ? { reasoning: data.reasoningTokens } : {}
206
+ };
207
+ return {
208
+ input_tokens: data.inputTokens,
209
+ output_tokens: data.outputTokens,
210
+ total_tokens: data.totalTokens,
211
+ ...Object.keys(inputTokenDetails).length > 0 ? { input_token_details: inputTokenDetails } : {},
212
+ ...Object.keys(outputTokenDetails).length > 0 ? { output_token_details: outputTokenDetails } : {}
213
+ };
214
+ }
215
+ __name(createUsageMetadata, "createUsageMetadata");
216
+ function openAIUsageToUsageMetadata(usage) {
217
+ return createUsageMetadata({
218
+ inputTokens: usage.prompt_tokens,
219
+ outputTokens: usage.completion_tokens,
220
+ totalTokens: usage.total_tokens,
221
+ inputAudioTokens: usage.prompt_tokens_details?.audio_tokens,
222
+ outputAudioTokens: usage.completion_tokens_details?.audio_tokens,
223
+ cacheReadTokens: usage.prompt_tokens_details?.cached_tokens,
224
+ reasoningTokens: usage.completion_tokens_details?.reasoning_tokens
225
+ });
226
+ }
227
+ __name(openAIUsageToUsageMetadata, "openAIUsageToUsageMetadata");
228
+ async function langchainMessageToOpenAIMessage(messages, plugin, model, supportImageInputType, removeSystemMessage) {
194
229
  const result = [];
195
230
  const normalizedModel = model ? normalizeOpenAIModelName(model) : model;
196
231
  const isDeepseekThinkModel = normalizedModel?.includes("deepseek-reasoner");
@@ -224,7 +259,7 @@ async function langchainMessageToOpenAIMessage(messages, plugin, model, supportI
224
259
  }
225
260
  const images = rawMessage.additional_kwargs.images;
226
261
  const lowerModel = normalizedModel?.toLowerCase() ?? "";
227
- if ((lowerModel?.includes("vision") || lowerModel?.includes("gpt-4o") || lowerModel?.includes("claude") || lowerModel?.includes("gemini") || lowerModel?.includes("qwen-vl") || lowerModel?.includes("omni") || lowerModel?.includes("qwen2.5-vl") || lowerModel?.includes("qwen2.5-omni") || lowerModel?.includes("qwen-omni") || lowerModel?.includes("qwen2-vl") || lowerModel?.includes("qwen3.5") || lowerModel?.includes("qvq") || normalizedModel?.includes("o1") || normalizedModel?.includes("o4") || normalizedModel?.includes("o3") || normalizedModel?.includes("gpt-4.1") || normalizedModel?.includes("gpt-5") || supportImageInput2) && images != null) {
262
+ if (images != null && (supportImageInput(lowerModel) || supportImageInputType)) {
228
263
  msg.content = [
229
264
  {
230
265
  type: "text",
@@ -376,15 +411,73 @@ async function fetchImageUrl(plugin, content) {
376
411
  }
377
412
  const ext = url.match(/\.([^.?#]+)(?:[?#]|$)/)?.[1]?.toLowerCase();
378
413
  const imageType = (0, import_string.getImageMimeType)(ext);
379
- const buffer = await plugin.fetch(url).then((res) => {
380
- if (!res.ok) {
381
- throw new Error(`Failed to fetch image: ${res.status}`);
382
- }
383
- return res.arrayBuffer();
384
- }).then(Buffer.from);
414
+ const controller = new AbortController();
415
+ const timeout = setTimeout(() => controller.abort(), 6e4);
416
+ const response = await plugin.fetch(url, {
417
+ signal: controller.signal
418
+ }).finally(() => {
419
+ clearTimeout(timeout);
420
+ });
421
+ if (!response.ok) {
422
+ throw new Error(`Failed to fetch image: ${response.status}`);
423
+ }
424
+ const buffer = Buffer.from(await response.arrayBuffer());
385
425
  return `data:${imageType};base64,${buffer.toString("base64")}`;
386
426
  }
387
427
  __name(fetchImageUrl, "fetchImageUrl");
428
+ function getFileLikeUrlInfo(content) {
429
+ switch (content.type) {
430
+ case "file_url": {
431
+ const raw = content.file_url;
432
+ return {
433
+ url: typeof raw === "string" ? raw : raw.url,
434
+ mimeType: typeof raw === "string" ? void 0 : raw.mimeType
435
+ };
436
+ }
437
+ case "audio_url": {
438
+ const raw = content.audio_url;
439
+ return {
440
+ url: typeof raw === "string" ? raw : raw.url,
441
+ mimeType: typeof raw === "string" ? void 0 : raw.mimeType
442
+ };
443
+ }
444
+ case "video_url": {
445
+ const raw = content.video_url;
446
+ return {
447
+ url: typeof raw === "string" ? raw : raw.url,
448
+ mimeType: typeof raw === "string" ? void 0 : raw.mimeType
449
+ };
450
+ }
451
+ }
452
+ }
453
+ __name(getFileLikeUrlInfo, "getFileLikeUrlInfo");
454
+ async function fetchFileLikeUrl(plugin, content) {
455
+ const { url, mimeType } = getFileLikeUrlInfo(content);
456
+ const dataUrlMatch = url.match(/^data:([^;,]+);base64,(.+)$/i);
457
+ if (dataUrlMatch) {
458
+ return {
459
+ buffer: Buffer.from(dataUrlMatch[2], "base64"),
460
+ mimeType: dataUrlMatch[1] || mimeType || "application/octet-stream"
461
+ };
462
+ }
463
+ const controller = new AbortController();
464
+ const timeout = setTimeout(() => controller.abort(), 6e4);
465
+ const response = await plugin.fetch(url, {
466
+ signal: controller.signal
467
+ }).finally(() => {
468
+ clearTimeout(timeout);
469
+ });
470
+ if (!response.ok) {
471
+ throw new Error(`Failed to fetch file: ${response.status}`);
472
+ }
473
+ const buffer = Buffer.from(await response.arrayBuffer());
474
+ const fetchedMimeType = response.headers.get("content-type")?.split(";")[0]?.trim();
475
+ return {
476
+ buffer,
477
+ mimeType: mimeType ?? fetchedMimeType ?? (0, import_string.getMimeTypeFromSource)(url) ?? "application/octet-stream"
478
+ };
479
+ }
480
+ __name(fetchFileLikeUrl, "fetchFileLikeUrl");
388
481
  function messageTypeToOpenAIRole(type) {
389
482
  switch (type) {
390
483
  case "system":
@@ -478,8 +571,12 @@ function convertMessageToMessageChunk(message) {
478
571
  const toolCallChunks = [];
479
572
  if (Array.isArray(message.tool_calls)) {
480
573
  for (const rawToolCall of message.tool_calls) {
574
+ let name = rawToolCall.function?.name;
575
+ if (name != null && name.length < 1) {
576
+ name = void 0;
577
+ }
481
578
  toolCallChunks.push({
482
- name: rawToolCall.function?.name,
579
+ name,
483
580
  args: rawToolCall.function?.arguments,
484
581
  id: rawToolCall.id
485
582
  });
@@ -518,10 +615,6 @@ function convertDeltaToMessageChunk(delta, defaultRole) {
518
615
  additionalKwargs = {
519
616
  function_call: delta.function_call
520
617
  };
521
- } else if (delta.tool_calls) {
522
- additionalKwargs = {
523
- tool_calls: delta.tool_calls
524
- };
525
618
  } else {
526
619
  additionalKwargs = {};
527
620
  }
@@ -534,12 +627,16 @@ function convertDeltaToMessageChunk(delta, defaultRole) {
534
627
  const toolCallChunks = [];
535
628
  if (Array.isArray(delta.tool_calls)) {
536
629
  for (const rawToolCall of delta.tool_calls) {
537
- toolCallChunks.push({
630
+ const toolCall = {
538
631
  name: rawToolCall.function?.name,
539
632
  args: rawToolCall.function?.arguments,
540
633
  id: rawToolCall.id,
541
634
  index: rawToolCall.index
542
- });
635
+ };
636
+ if (toolCall.name != null && toolCall.name.length < 1) {
637
+ delete toolCall.name;
638
+ }
639
+ toolCallChunks.push(toolCall);
543
640
  }
544
641
  }
545
642
  return new import_messages.AIMessageChunk({
@@ -619,25 +716,15 @@ async function buildChatCompletionParams(params, plugin, enableGoogleSearch, sup
619
716
  return (0, import_object.deepAssign)({}, base, params.overrideRequestParams ?? {});
620
717
  }
621
718
  __name(buildChatCompletionParams, "buildChatCompletionParams");
622
- function processReasoningContent(delta, reasoningState) {
623
- if (delta.reasoning_content) {
624
- reasoningState.content += delta.reasoning_content;
625
- if (reasoningState.time === 0) {
626
- reasoningState.time = Date.now();
627
- }
628
- }
629
- if ((delta.reasoning_content == null || delta.reasoning_content === "") && delta.content && delta.content.length > 0 && reasoningState.time > 0 && !reasoningState.isSet) {
630
- const reasoningTime = Date.now() - reasoningState.time;
631
- reasoningState.time = reasoningTime;
632
- reasoningState.isSet = true;
633
- return reasoningTime;
634
- }
635
- }
636
- __name(processReasoningContent, "processReasoningContent");
637
719
  async function* processStreamResponse(requestContext, iterator) {
638
720
  let defaultRole = "assistant";
639
721
  let errorCount = 0;
640
- const reasoningState = { content: "", time: 0, isSet: false };
722
+ const reasoningState = {
723
+ content: "",
724
+ startedAt: 0,
725
+ duration: 0,
726
+ done: false
727
+ };
641
728
  for await (const event of iterator) {
642
729
  const chunk = event.data;
643
730
  if (chunk === "[DONE]") break;
@@ -652,16 +739,14 @@ async function* processStreamResponse(requestContext, iterator) {
652
739
  }
653
740
  const choice = data.choices?.[0];
654
741
  if (data.usage) {
742
+ const usageMetadata = openAIUsageToUsageMetadata(data.usage);
655
743
  yield new import_outputs.ChatGenerationChunk({
744
+ generationInfo: {
745
+ usage_metadata: usageMetadata
746
+ },
656
747
  message: new import_messages2.AIMessageChunk({
657
748
  content: "",
658
- response_metadata: {
659
- tokenUsage: {
660
- promptTokens: data.usage.prompt_tokens,
661
- completionTokens: data.usage.completion_tokens,
662
- totalTokens: data.usage.total_tokens
663
- }
664
- }
749
+ usage_metadata: usageMetadata
665
750
  }),
666
751
  text: ""
667
752
  });
@@ -669,9 +754,16 @@ async function* processStreamResponse(requestContext, iterator) {
669
754
  if (!choice) continue;
670
755
  const { delta } = choice;
671
756
  const messageChunk = convertDeltaToMessageChunk(delta, defaultRole);
672
- const reasoningTime = processReasoningContent(delta, reasoningState);
673
- if (reasoningTime !== void 0) {
674
- messageChunk.additional_kwargs.reasoning_time = reasoningTime;
757
+ if (delta.reasoning_content) {
758
+ reasoningState.content += delta.reasoning_content;
759
+ if (reasoningState.startedAt === 0) {
760
+ reasoningState.startedAt = Date.now();
761
+ }
762
+ }
763
+ if (!reasoningState.done && reasoningState.startedAt > 0 && ((delta.content?.length ?? 0) > 0 || (delta.tool_calls?.length ?? 0) > 0 || delta.function_call != null)) {
764
+ reasoningState.duration = Date.now() - reasoningState.startedAt;
765
+ reasoningState.done = true;
766
+ messageChunk.additional_kwargs.reasoning_time = reasoningState.duration;
675
767
  }
676
768
  defaultRole = (delta.role?.length ?? 0) > 0 ? delta.role : defaultRole;
677
769
  yield new import_outputs.ChatGenerationChunk({
@@ -679,6 +771,13 @@ async function* processStreamResponse(requestContext, iterator) {
679
771
  text: messageChunk.content
680
772
  });
681
773
  } catch (e) {
774
+ if (chunk.includes("tool_calls") || chunk.includes("function_call") || chunk.includes("tool_call_id")) {
775
+ requestContext.modelRequester.logger.error(
776
+ "error with chunk",
777
+ chunk
778
+ );
779
+ throw new import_error.ChatLunaError(import_error.ChatLunaErrorCode.API_REQUEST_FAILED, e);
780
+ }
682
781
  if (errorCount > 5) {
683
782
  requestContext.modelRequester.logger.error(
684
783
  "error with chunk",
@@ -690,8 +789,21 @@ async function* processStreamResponse(requestContext, iterator) {
690
789
  }
691
790
  }
692
791
  if (reasoningState.content.length > 0) {
792
+ if (!reasoningState.done && reasoningState.startedAt > 0) {
793
+ reasoningState.duration = Date.now() - reasoningState.startedAt;
794
+ reasoningState.done = true;
795
+ yield new import_outputs.ChatGenerationChunk({
796
+ message: new import_messages2.AIMessageChunk({
797
+ content: "",
798
+ additional_kwargs: {
799
+ reasoning_time: reasoningState.duration
800
+ }
801
+ }),
802
+ text: ""
803
+ });
804
+ }
693
805
  requestContext.modelRequester.logger.debug(
694
- `reasoning content: ${reasoningState.content}. Use time: ${reasoningState.time / 1e3}s`
806
+ `reasoning content: ${reasoningState.content}. Use time: ${reasoningState.duration / 1e3}s`
695
807
  );
696
808
  }
697
809
  }
@@ -726,11 +838,15 @@ async function processResponse(requestContext, response) {
726
838
  );
727
839
  }
728
840
  const messageChunk = convertMessageToMessageChunk(choice.message);
841
+ const usageMetadata = data.usage ? openAIUsageToUsageMetadata(data.usage) : void 0;
842
+ if (messageChunk instanceof import_messages2.AIMessageChunk) {
843
+ messageChunk.usage_metadata = usageMetadata;
844
+ }
729
845
  return new import_outputs.ChatGenerationChunk({
730
846
  message: messageChunk,
731
847
  text: (0, import_string2.getMessageContent)(messageChunk.content),
732
- generationInfo: {
733
- tokenUsage: data.usage
848
+ generationInfo: usageMetadata == null ? void 0 : {
849
+ usage_metadata: usageMetadata
734
850
  }
735
851
  });
736
852
  } catch (e) {
@@ -898,7 +1014,9 @@ __name(createRequestContext, "createRequestContext");
898
1014
  convertMessageToMessageChunk,
899
1015
  createEmbeddings,
900
1016
  createRequestContext,
1017
+ createUsageMetadata,
901
1018
  expandReasoningEffortModelVariants,
1019
+ fetchFileLikeUrl,
902
1020
  fetchImageUrl,
903
1021
  formatToolToOpenAITool,
904
1022
  formatToolsToOpenAITools,
@@ -909,9 +1027,9 @@ __name(createRequestContext, "createRequestContext");
909
1027
  langchainMessageToOpenAIMessage,
910
1028
  messageTypeToOpenAIRole,
911
1029
  normalizeOpenAIModelName,
1030
+ openAIUsageToUsageMetadata,
912
1031
  parseOpenAIModelNameWithReasoningEffort,
913
1032
  processDeepSeekThinkMessages,
914
- processReasoningContent,
915
1033
  processResponse,
916
1034
  processStreamResponse,
917
1035
  reasoningEffortModelSuffixes,
package/lib/index.mjs CHANGED
@@ -114,6 +114,8 @@ var imageModelMatchers = [
114
114
  "qwen*-omni",
115
115
  "qwen-omni",
116
116
  "qwen*-vl",
117
+ "qwen-3.5",
118
+ "qwen3.5",
117
119
  "qvq",
118
120
  "o1",
119
121
  "o3",
@@ -151,10 +153,42 @@ import {
151
153
  import { zodToJsonSchema } from "zod-to-json-schema";
152
154
  import {
153
155
  getImageMimeType,
156
+ getMimeTypeFromSource,
154
157
  isMessageContentImageUrl
155
158
  } from "koishi-plugin-chatluna/utils/string";
156
159
  import { isZodSchemaV3 } from "@langchain/core/utils/types";
157
- async function langchainMessageToOpenAIMessage(messages, plugin, model, supportImageInput2, removeSystemMessage) {
160
+ function createUsageMetadata(data) {
161
+ const inputTokenDetails = {
162
+ ...data.inputAudioTokens != null ? { audio: data.inputAudioTokens } : {},
163
+ ...data.cacheReadTokens != null ? { cache_read: data.cacheReadTokens } : {},
164
+ ...data.cacheCreationTokens != null ? { cache_creation: data.cacheCreationTokens } : {}
165
+ };
166
+ const outputTokenDetails = {
167
+ ...data.outputAudioTokens != null ? { audio: data.outputAudioTokens } : {},
168
+ ...data.reasoningTokens != null ? { reasoning: data.reasoningTokens } : {}
169
+ };
170
+ return {
171
+ input_tokens: data.inputTokens,
172
+ output_tokens: data.outputTokens,
173
+ total_tokens: data.totalTokens,
174
+ ...Object.keys(inputTokenDetails).length > 0 ? { input_token_details: inputTokenDetails } : {},
175
+ ...Object.keys(outputTokenDetails).length > 0 ? { output_token_details: outputTokenDetails } : {}
176
+ };
177
+ }
178
+ __name(createUsageMetadata, "createUsageMetadata");
179
+ function openAIUsageToUsageMetadata(usage) {
180
+ return createUsageMetadata({
181
+ inputTokens: usage.prompt_tokens,
182
+ outputTokens: usage.completion_tokens,
183
+ totalTokens: usage.total_tokens,
184
+ inputAudioTokens: usage.prompt_tokens_details?.audio_tokens,
185
+ outputAudioTokens: usage.completion_tokens_details?.audio_tokens,
186
+ cacheReadTokens: usage.prompt_tokens_details?.cached_tokens,
187
+ reasoningTokens: usage.completion_tokens_details?.reasoning_tokens
188
+ });
189
+ }
190
+ __name(openAIUsageToUsageMetadata, "openAIUsageToUsageMetadata");
191
+ async function langchainMessageToOpenAIMessage(messages, plugin, model, supportImageInputType, removeSystemMessage) {
158
192
  const result = [];
159
193
  const normalizedModel = model ? normalizeOpenAIModelName(model) : model;
160
194
  const isDeepseekThinkModel = normalizedModel?.includes("deepseek-reasoner");
@@ -188,7 +222,7 @@ async function langchainMessageToOpenAIMessage(messages, plugin, model, supportI
188
222
  }
189
223
  const images = rawMessage.additional_kwargs.images;
190
224
  const lowerModel = normalizedModel?.toLowerCase() ?? "";
191
- if ((lowerModel?.includes("vision") || lowerModel?.includes("gpt-4o") || lowerModel?.includes("claude") || lowerModel?.includes("gemini") || lowerModel?.includes("qwen-vl") || lowerModel?.includes("omni") || lowerModel?.includes("qwen2.5-vl") || lowerModel?.includes("qwen2.5-omni") || lowerModel?.includes("qwen-omni") || lowerModel?.includes("qwen2-vl") || lowerModel?.includes("qwen3.5") || lowerModel?.includes("qvq") || normalizedModel?.includes("o1") || normalizedModel?.includes("o4") || normalizedModel?.includes("o3") || normalizedModel?.includes("gpt-4.1") || normalizedModel?.includes("gpt-5") || supportImageInput2) && images != null) {
225
+ if (images != null && (supportImageInput(lowerModel) || supportImageInputType)) {
192
226
  msg.content = [
193
227
  {
194
228
  type: "text",
@@ -340,15 +374,73 @@ async function fetchImageUrl(plugin, content) {
340
374
  }
341
375
  const ext = url.match(/\.([^.?#]+)(?:[?#]|$)/)?.[1]?.toLowerCase();
342
376
  const imageType = getImageMimeType(ext);
343
- const buffer = await plugin.fetch(url).then((res) => {
344
- if (!res.ok) {
345
- throw new Error(`Failed to fetch image: ${res.status}`);
346
- }
347
- return res.arrayBuffer();
348
- }).then(Buffer.from);
377
+ const controller = new AbortController();
378
+ const timeout = setTimeout(() => controller.abort(), 6e4);
379
+ const response = await plugin.fetch(url, {
380
+ signal: controller.signal
381
+ }).finally(() => {
382
+ clearTimeout(timeout);
383
+ });
384
+ if (!response.ok) {
385
+ throw new Error(`Failed to fetch image: ${response.status}`);
386
+ }
387
+ const buffer = Buffer.from(await response.arrayBuffer());
349
388
  return `data:${imageType};base64,${buffer.toString("base64")}`;
350
389
  }
351
390
  __name(fetchImageUrl, "fetchImageUrl");
391
+ function getFileLikeUrlInfo(content) {
392
+ switch (content.type) {
393
+ case "file_url": {
394
+ const raw = content.file_url;
395
+ return {
396
+ url: typeof raw === "string" ? raw : raw.url,
397
+ mimeType: typeof raw === "string" ? void 0 : raw.mimeType
398
+ };
399
+ }
400
+ case "audio_url": {
401
+ const raw = content.audio_url;
402
+ return {
403
+ url: typeof raw === "string" ? raw : raw.url,
404
+ mimeType: typeof raw === "string" ? void 0 : raw.mimeType
405
+ };
406
+ }
407
+ case "video_url": {
408
+ const raw = content.video_url;
409
+ return {
410
+ url: typeof raw === "string" ? raw : raw.url,
411
+ mimeType: typeof raw === "string" ? void 0 : raw.mimeType
412
+ };
413
+ }
414
+ }
415
+ }
416
+ __name(getFileLikeUrlInfo, "getFileLikeUrlInfo");
417
+ async function fetchFileLikeUrl(plugin, content) {
418
+ const { url, mimeType } = getFileLikeUrlInfo(content);
419
+ const dataUrlMatch = url.match(/^data:([^;,]+);base64,(.+)$/i);
420
+ if (dataUrlMatch) {
421
+ return {
422
+ buffer: Buffer.from(dataUrlMatch[2], "base64"),
423
+ mimeType: dataUrlMatch[1] || mimeType || "application/octet-stream"
424
+ };
425
+ }
426
+ const controller = new AbortController();
427
+ const timeout = setTimeout(() => controller.abort(), 6e4);
428
+ const response = await plugin.fetch(url, {
429
+ signal: controller.signal
430
+ }).finally(() => {
431
+ clearTimeout(timeout);
432
+ });
433
+ if (!response.ok) {
434
+ throw new Error(`Failed to fetch file: ${response.status}`);
435
+ }
436
+ const buffer = Buffer.from(await response.arrayBuffer());
437
+ const fetchedMimeType = response.headers.get("content-type")?.split(";")[0]?.trim();
438
+ return {
439
+ buffer,
440
+ mimeType: mimeType ?? fetchedMimeType ?? getMimeTypeFromSource(url) ?? "application/octet-stream"
441
+ };
442
+ }
443
+ __name(fetchFileLikeUrl, "fetchFileLikeUrl");
352
444
  function messageTypeToOpenAIRole(type) {
353
445
  switch (type) {
354
446
  case "system":
@@ -442,8 +534,12 @@ function convertMessageToMessageChunk(message) {
442
534
  const toolCallChunks = [];
443
535
  if (Array.isArray(message.tool_calls)) {
444
536
  for (const rawToolCall of message.tool_calls) {
537
+ let name = rawToolCall.function?.name;
538
+ if (name != null && name.length < 1) {
539
+ name = void 0;
540
+ }
445
541
  toolCallChunks.push({
446
- name: rawToolCall.function?.name,
542
+ name,
447
543
  args: rawToolCall.function?.arguments,
448
544
  id: rawToolCall.id
449
545
  });
@@ -482,10 +578,6 @@ function convertDeltaToMessageChunk(delta, defaultRole) {
482
578
  additionalKwargs = {
483
579
  function_call: delta.function_call
484
580
  };
485
- } else if (delta.tool_calls) {
486
- additionalKwargs = {
487
- tool_calls: delta.tool_calls
488
- };
489
581
  } else {
490
582
  additionalKwargs = {};
491
583
  }
@@ -498,12 +590,16 @@ function convertDeltaToMessageChunk(delta, defaultRole) {
498
590
  const toolCallChunks = [];
499
591
  if (Array.isArray(delta.tool_calls)) {
500
592
  for (const rawToolCall of delta.tool_calls) {
501
- toolCallChunks.push({
593
+ const toolCall = {
502
594
  name: rawToolCall.function?.name,
503
595
  args: rawToolCall.function?.arguments,
504
596
  id: rawToolCall.id,
505
597
  index: rawToolCall.index
506
- });
598
+ };
599
+ if (toolCall.name != null && toolCall.name.length < 1) {
600
+ delete toolCall.name;
601
+ }
602
+ toolCallChunks.push(toolCall);
507
603
  }
508
604
  }
509
605
  return new AIMessageChunk({
@@ -583,25 +679,15 @@ async function buildChatCompletionParams(params, plugin, enableGoogleSearch, sup
583
679
  return deepAssign({}, base, params.overrideRequestParams ?? {});
584
680
  }
585
681
  __name(buildChatCompletionParams, "buildChatCompletionParams");
586
- function processReasoningContent(delta, reasoningState) {
587
- if (delta.reasoning_content) {
588
- reasoningState.content += delta.reasoning_content;
589
- if (reasoningState.time === 0) {
590
- reasoningState.time = Date.now();
591
- }
592
- }
593
- if ((delta.reasoning_content == null || delta.reasoning_content === "") && delta.content && delta.content.length > 0 && reasoningState.time > 0 && !reasoningState.isSet) {
594
- const reasoningTime = Date.now() - reasoningState.time;
595
- reasoningState.time = reasoningTime;
596
- reasoningState.isSet = true;
597
- return reasoningTime;
598
- }
599
- }
600
- __name(processReasoningContent, "processReasoningContent");
601
682
  async function* processStreamResponse(requestContext, iterator) {
602
683
  let defaultRole = "assistant";
603
684
  let errorCount = 0;
604
- const reasoningState = { content: "", time: 0, isSet: false };
685
+ const reasoningState = {
686
+ content: "",
687
+ startedAt: 0,
688
+ duration: 0,
689
+ done: false
690
+ };
605
691
  for await (const event of iterator) {
606
692
  const chunk = event.data;
607
693
  if (chunk === "[DONE]") break;
@@ -616,16 +702,14 @@ async function* processStreamResponse(requestContext, iterator) {
616
702
  }
617
703
  const choice = data.choices?.[0];
618
704
  if (data.usage) {
705
+ const usageMetadata = openAIUsageToUsageMetadata(data.usage);
619
706
  yield new ChatGenerationChunk({
707
+ generationInfo: {
708
+ usage_metadata: usageMetadata
709
+ },
620
710
  message: new AIMessageChunk2({
621
711
  content: "",
622
- response_metadata: {
623
- tokenUsage: {
624
- promptTokens: data.usage.prompt_tokens,
625
- completionTokens: data.usage.completion_tokens,
626
- totalTokens: data.usage.total_tokens
627
- }
628
- }
712
+ usage_metadata: usageMetadata
629
713
  }),
630
714
  text: ""
631
715
  });
@@ -633,9 +717,16 @@ async function* processStreamResponse(requestContext, iterator) {
633
717
  if (!choice) continue;
634
718
  const { delta } = choice;
635
719
  const messageChunk = convertDeltaToMessageChunk(delta, defaultRole);
636
- const reasoningTime = processReasoningContent(delta, reasoningState);
637
- if (reasoningTime !== void 0) {
638
- messageChunk.additional_kwargs.reasoning_time = reasoningTime;
720
+ if (delta.reasoning_content) {
721
+ reasoningState.content += delta.reasoning_content;
722
+ if (reasoningState.startedAt === 0) {
723
+ reasoningState.startedAt = Date.now();
724
+ }
725
+ }
726
+ if (!reasoningState.done && reasoningState.startedAt > 0 && ((delta.content?.length ?? 0) > 0 || (delta.tool_calls?.length ?? 0) > 0 || delta.function_call != null)) {
727
+ reasoningState.duration = Date.now() - reasoningState.startedAt;
728
+ reasoningState.done = true;
729
+ messageChunk.additional_kwargs.reasoning_time = reasoningState.duration;
639
730
  }
640
731
  defaultRole = (delta.role?.length ?? 0) > 0 ? delta.role : defaultRole;
641
732
  yield new ChatGenerationChunk({
@@ -643,6 +734,13 @@ async function* processStreamResponse(requestContext, iterator) {
643
734
  text: messageChunk.content
644
735
  });
645
736
  } catch (e) {
737
+ if (chunk.includes("tool_calls") || chunk.includes("function_call") || chunk.includes("tool_call_id")) {
738
+ requestContext.modelRequester.logger.error(
739
+ "error with chunk",
740
+ chunk
741
+ );
742
+ throw new ChatLunaError(ChatLunaErrorCode.API_REQUEST_FAILED, e);
743
+ }
646
744
  if (errorCount > 5) {
647
745
  requestContext.modelRequester.logger.error(
648
746
  "error with chunk",
@@ -654,8 +752,21 @@ async function* processStreamResponse(requestContext, iterator) {
654
752
  }
655
753
  }
656
754
  if (reasoningState.content.length > 0) {
755
+ if (!reasoningState.done && reasoningState.startedAt > 0) {
756
+ reasoningState.duration = Date.now() - reasoningState.startedAt;
757
+ reasoningState.done = true;
758
+ yield new ChatGenerationChunk({
759
+ message: new AIMessageChunk2({
760
+ content: "",
761
+ additional_kwargs: {
762
+ reasoning_time: reasoningState.duration
763
+ }
764
+ }),
765
+ text: ""
766
+ });
767
+ }
657
768
  requestContext.modelRequester.logger.debug(
658
- `reasoning content: ${reasoningState.content}. Use time: ${reasoningState.time / 1e3}s`
769
+ `reasoning content: ${reasoningState.content}. Use time: ${reasoningState.duration / 1e3}s`
659
770
  );
660
771
  }
661
772
  }
@@ -690,11 +801,15 @@ async function processResponse(requestContext, response) {
690
801
  );
691
802
  }
692
803
  const messageChunk = convertMessageToMessageChunk(choice.message);
804
+ const usageMetadata = data.usage ? openAIUsageToUsageMetadata(data.usage) : void 0;
805
+ if (messageChunk instanceof AIMessageChunk2) {
806
+ messageChunk.usage_metadata = usageMetadata;
807
+ }
693
808
  return new ChatGenerationChunk({
694
809
  message: messageChunk,
695
810
  text: getMessageContent(messageChunk.content),
696
- generationInfo: {
697
- tokenUsage: data.usage
811
+ generationInfo: usageMetadata == null ? void 0 : {
812
+ usage_metadata: usageMetadata
698
813
  }
699
814
  });
700
815
  } catch (e) {
@@ -861,7 +976,9 @@ export {
861
976
  convertMessageToMessageChunk,
862
977
  createEmbeddings,
863
978
  createRequestContext,
979
+ createUsageMetadata,
864
980
  expandReasoningEffortModelVariants,
981
+ fetchFileLikeUrl,
865
982
  fetchImageUrl,
866
983
  formatToolToOpenAITool,
867
984
  formatToolsToOpenAITools,
@@ -872,9 +989,9 @@ export {
872
989
  langchainMessageToOpenAIMessage,
873
990
  messageTypeToOpenAIRole,
874
991
  normalizeOpenAIModelName,
992
+ openAIUsageToUsageMetadata,
875
993
  parseOpenAIModelNameWithReasoningEffort,
876
994
  processDeepSeekThinkMessages,
877
- processReasoningContent,
878
995
  processResponse,
879
996
  processStreamResponse,
880
997
  reasoningEffortModelSuffixes,
@@ -37,14 +37,6 @@ export declare function buildChatCompletionParams(params: ModelRequestParams, pl
37
37
  include_usage: boolean;
38
38
  };
39
39
  } & Record<string, any>>;
40
- export declare function processReasoningContent(delta: {
41
- reasoning_content?: string;
42
- content?: string;
43
- }, reasoningState: {
44
- content: string;
45
- time: number;
46
- isSet: boolean;
47
- }): number;
48
40
  export declare function processStreamResponse<T extends ClientConfig, R extends ChatLunaPlugin.Config>(requestContext: RequestContext<T, R>, iterator: AsyncGenerator<SSEEvent, string, unknown>): AsyncGenerator<ChatGenerationChunk, void, unknown>;
49
41
  export declare function processResponse<T extends ClientConfig, R extends ChatLunaPlugin.Config>(requestContext: RequestContext<T, R>, response: Response): Promise<ChatGenerationChunk>;
50
42
  export declare function completionStream<T extends ClientConfig, R extends ChatLunaPlugin.Config>(requestContext: RequestContext<T, R>, params: ModelRequestParams, completionUrl?: string, enableGoogleSearch?: boolean, supportImageInput?: boolean): AsyncGenerator<ChatGenerationChunk>;
package/lib/types.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface ChatCompletionResponse {
7
7
  role?: string;
8
8
  reasoning_content?: string;
9
9
  function_call?: ChatCompletionRequestMessageToolCall;
10
+ tool_calls?: ChatCompletionRequestMessageToolCall[];
10
11
  };
11
12
  message: ChatCompletionResponseMessage;
12
13
  }[];
@@ -14,11 +15,24 @@ export interface ChatCompletionResponse {
14
15
  object: string;
15
16
  created: number;
16
17
  model: string;
17
- usage: {
18
- prompt_tokens: number;
19
- completion_tokens: number;
20
- total_tokens: number;
21
- };
18
+ usage?: ChatCompletionUsage;
19
+ }
20
+ export interface ChatCompletionPromptTokensDetails {
21
+ audio_tokens?: number;
22
+ cached_tokens?: number;
23
+ }
24
+ export interface ChatCompletionCompletionTokensDetails {
25
+ reasoning_tokens?: number;
26
+ audio_tokens?: number;
27
+ accepted_prediction_tokens?: number;
28
+ rejected_prediction_tokens?: number;
29
+ }
30
+ export interface ChatCompletionUsage {
31
+ prompt_tokens: number;
32
+ completion_tokens: number;
33
+ total_tokens: number;
34
+ prompt_tokens_details?: ChatCompletionPromptTokensDetails;
35
+ completion_tokens_details?: ChatCompletionCompletionTokensDetails;
22
36
  }
23
37
  export interface ChatCompletionTextPart {
24
38
  type: 'text';
package/lib/utils.d.ts CHANGED
@@ -1,15 +1,54 @@
1
- import { AIMessageChunk, BaseMessage, ChatMessageChunk, FunctionMessageChunk, HumanMessageChunk, MessageContentImageUrl, MessageType, SystemMessageChunk, ToolMessageChunk } from '@langchain/core/messages';
1
+ import { AIMessageChunk, BaseMessage, ChatMessageChunk, FunctionMessageChunk, HumanMessageChunk, MessageContentComplex, MessageContentImageUrl, MessageType, SystemMessageChunk, type UsageMetadata, ToolMessageChunk } from '@langchain/core/messages';
2
2
  import { StructuredTool } from '@langchain/core/tools';
3
3
  import { JsonSchema7Type } from 'zod-to-json-schema';
4
- import { ChatCompletionResponseMessage, ChatCompletionResponseMessageRoleEnum, ChatCompletionTool } from './types';
4
+ import { ChatCompletionUsage, ChatCompletionResponseMessage, ChatCompletionResponseMessageRoleEnum, ChatCompletionTool } from './types';
5
5
  import { ChatLunaPlugin } from 'koishi-plugin-chatluna/services/chat';
6
- export declare function langchainMessageToOpenAIMessage(messages: BaseMessage[], plugin: ChatLunaPlugin, model?: string, supportImageInput?: boolean, removeSystemMessage?: boolean): Promise<ChatCompletionResponseMessage[]>;
6
+ export declare function createUsageMetadata(data: {
7
+ inputTokens: number;
8
+ outputTokens: number;
9
+ totalTokens: number;
10
+ inputAudioTokens?: number;
11
+ outputAudioTokens?: number;
12
+ cacheReadTokens?: number;
13
+ cacheCreationTokens?: number;
14
+ reasoningTokens?: number;
15
+ }): UsageMetadata;
16
+ export declare function openAIUsageToUsageMetadata(usage: ChatCompletionUsage): UsageMetadata;
17
+ export declare function langchainMessageToOpenAIMessage(messages: BaseMessage[], plugin: ChatLunaPlugin, model?: string, supportImageInputType?: boolean, removeSystemMessage?: boolean): Promise<ChatCompletionResponseMessage[]>;
7
18
  export declare function processDeepSeekThinkMessages(convertedMessages: ChatCompletionResponseMessage[], originalMessages: BaseMessage[]): ChatCompletionResponseMessage[];
8
19
  export declare function transformSystemMessages(messages: ChatCompletionResponseMessage[]): ChatCompletionResponseMessage[];
9
20
  export declare function fetchImageUrl(plugin: ChatLunaPlugin, content: MessageContentImageUrl): Promise<string>;
21
+ type MessageContentFileLike = MessageContentComplex & ({
22
+ type: 'file_url';
23
+ file_url: string | {
24
+ url: string;
25
+ mimeType?: string;
26
+ };
27
+ } | {
28
+ type: 'audio_url';
29
+ audio_url: string | {
30
+ url: string;
31
+ mimeType?: string;
32
+ };
33
+ } | {
34
+ type: 'video_url';
35
+ video_url: string | {
36
+ url: string;
37
+ mimeType?: string;
38
+ };
39
+ });
40
+ /**
41
+ * Fetch file/audio/video content and return decoded bytes.
42
+ * If the source is a base64 data URL, it is decoded directly.
43
+ */
44
+ export declare function fetchFileLikeUrl(plugin: ChatLunaPlugin, content: MessageContentFileLike): Promise<{
45
+ buffer: Buffer<ArrayBuffer>;
46
+ mimeType: string;
47
+ }>;
10
48
  export declare function messageTypeToOpenAIRole(type: MessageType): ChatCompletionResponseMessageRoleEnum;
11
49
  export declare function formatToolsToOpenAITools(tools: StructuredTool[], includeGoogleSearch: boolean): ChatCompletionTool[];
12
50
  export declare function formatToolToOpenAITool(tool: StructuredTool): ChatCompletionTool;
13
51
  export declare function removeAdditionalProperties(schema: JsonSchema7Type): JsonSchema7Type;
14
52
  export declare function convertMessageToMessageChunk(message: ChatCompletionResponseMessage): HumanMessageChunk | AIMessageChunk | SystemMessageChunk | FunctionMessageChunk | ToolMessageChunk | ChatMessageChunk;
15
53
  export declare function convertDeltaToMessageChunk(delta: Record<string, any>, defaultRole?: ChatCompletionResponseMessageRoleEnum): HumanMessageChunk | AIMessageChunk | SystemMessageChunk | FunctionMessageChunk | ToolMessageChunk | ChatMessageChunk;
54
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chatluna/v1-shared-adapter",
3
3
  "description": "chatluna shared adapter",
4
- "version": "1.0.27",
4
+ "version": "1.0.29",
5
5
  "main": "lib/index.cjs",
6
6
  "module": "lib/index.mjs",
7
7
  "typings": "lib/index.d.ts",
@@ -70,6 +70,6 @@
70
70
  },
71
71
  "peerDependencies": {
72
72
  "koishi": "^4.18.9",
73
- "koishi-plugin-chatluna": "^1.3.22"
73
+ "koishi-plugin-chatluna": "^1.3.33"
74
74
  }
75
75
  }