@aituber-onair/chat 0.32.0 → 0.34.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.
Files changed (111) hide show
  1. package/README.ja.md +62 -4
  2. package/README.md +63 -4
  3. package/dist/cjs/constants/deepseek.d.ts +11 -0
  4. package/dist/cjs/constants/deepseek.d.ts.map +1 -0
  5. package/dist/cjs/constants/deepseek.js +22 -0
  6. package/dist/cjs/constants/deepseek.js.map +1 -0
  7. package/dist/cjs/constants/gemini.d.ts +11 -0
  8. package/dist/cjs/constants/gemini.d.ts.map +1 -1
  9. package/dist/cjs/constants/gemini.js +24 -7
  10. package/dist/cjs/constants/gemini.js.map +1 -1
  11. package/dist/cjs/constants/index.d.ts +2 -0
  12. package/dist/cjs/constants/index.d.ts.map +1 -1
  13. package/dist/cjs/constants/index.js +2 -0
  14. package/dist/cjs/constants/index.js.map +1 -1
  15. package/dist/cjs/constants/mistral.d.ts +16 -0
  16. package/dist/cjs/constants/mistral.d.ts.map +1 -0
  17. package/dist/cjs/constants/mistral.js +45 -0
  18. package/dist/cjs/constants/mistral.js.map +1 -0
  19. package/dist/cjs/index.d.ts +5 -1
  20. package/dist/cjs/index.d.ts.map +1 -1
  21. package/dist/cjs/index.js +11 -1
  22. package/dist/cjs/index.js.map +1 -1
  23. package/dist/cjs/services/providers/ChatServiceProvider.d.ts +9 -1
  24. package/dist/cjs/services/providers/ChatServiceProvider.d.ts.map +1 -1
  25. package/dist/cjs/services/providers/deepseek/DeepSeekChatService.d.ts +7 -0
  26. package/dist/cjs/services/providers/deepseek/DeepSeekChatService.d.ts.map +1 -0
  27. package/dist/cjs/services/providers/deepseek/DeepSeekChatService.js +12 -0
  28. package/dist/cjs/services/providers/deepseek/DeepSeekChatService.js.map +1 -0
  29. package/dist/cjs/services/providers/deepseek/DeepSeekChatServiceProvider.d.ts +16 -0
  30. package/dist/cjs/services/providers/deepseek/DeepSeekChatServiceProvider.d.ts.map +1 -0
  31. package/dist/cjs/services/providers/deepseek/DeepSeekChatServiceProvider.js +57 -0
  32. package/dist/cjs/services/providers/deepseek/DeepSeekChatServiceProvider.js.map +1 -0
  33. package/dist/cjs/services/providers/gemini/GeminiChatService.js +1 -1
  34. package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.d.ts.map +1 -1
  35. package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.js +2 -15
  36. package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.js.map +1 -1
  37. package/dist/cjs/services/providers/index.d.ts +3 -1
  38. package/dist/cjs/services/providers/index.d.ts.map +1 -1
  39. package/dist/cjs/services/providers/index.js +4 -0
  40. package/dist/cjs/services/providers/index.js.map +1 -1
  41. package/dist/cjs/services/providers/mistral/MistralChatService.d.ts +8 -0
  42. package/dist/cjs/services/providers/mistral/MistralChatService.d.ts.map +1 -0
  43. package/dist/cjs/services/providers/mistral/MistralChatService.js +12 -0
  44. package/dist/cjs/services/providers/mistral/MistralChatService.js.map +1 -0
  45. package/dist/cjs/services/providers/mistral/MistralChatServiceProvider.d.ts +17 -0
  46. package/dist/cjs/services/providers/mistral/MistralChatServiceProvider.d.ts.map +1 -0
  47. package/dist/cjs/services/providers/mistral/MistralChatServiceProvider.js +72 -0
  48. package/dist/cjs/services/providers/mistral/MistralChatServiceProvider.js.map +1 -0
  49. package/dist/cjs/services/providers/openai/OpenAIChatService.d.ts +2 -0
  50. package/dist/cjs/services/providers/openai/OpenAIChatService.d.ts.map +1 -1
  51. package/dist/cjs/services/providers/openai/OpenAIChatService.js +42 -4
  52. package/dist/cjs/services/providers/openai/OpenAIChatService.js.map +1 -1
  53. package/dist/cjs/utils/openaiCompatibleSse.d.ts.map +1 -1
  54. package/dist/cjs/utils/openaiCompatibleSse.js +32 -6
  55. package/dist/cjs/utils/openaiCompatibleSse.js.map +1 -1
  56. package/dist/esm/constants/deepseek.d.ts +11 -0
  57. package/dist/esm/constants/deepseek.d.ts.map +1 -0
  58. package/dist/esm/constants/deepseek.js +19 -0
  59. package/dist/esm/constants/deepseek.js.map +1 -0
  60. package/dist/esm/constants/gemini.d.ts +11 -0
  61. package/dist/esm/constants/gemini.d.ts.map +1 -1
  62. package/dist/esm/constants/gemini.js +23 -6
  63. package/dist/esm/constants/gemini.js.map +1 -1
  64. package/dist/esm/constants/index.d.ts +2 -0
  65. package/dist/esm/constants/index.d.ts.map +1 -1
  66. package/dist/esm/constants/index.js +2 -0
  67. package/dist/esm/constants/index.js.map +1 -1
  68. package/dist/esm/constants/mistral.d.ts +16 -0
  69. package/dist/esm/constants/mistral.d.ts.map +1 -0
  70. package/dist/esm/constants/mistral.js +39 -0
  71. package/dist/esm/constants/mistral.js.map +1 -0
  72. package/dist/esm/index.d.ts +5 -1
  73. package/dist/esm/index.d.ts.map +1 -1
  74. package/dist/esm/index.js +6 -0
  75. package/dist/esm/index.js.map +1 -1
  76. package/dist/esm/services/providers/ChatServiceProvider.d.ts +9 -1
  77. package/dist/esm/services/providers/ChatServiceProvider.d.ts.map +1 -1
  78. package/dist/esm/services/providers/deepseek/DeepSeekChatService.d.ts +7 -0
  79. package/dist/esm/services/providers/deepseek/DeepSeekChatService.d.ts.map +1 -0
  80. package/dist/esm/services/providers/deepseek/DeepSeekChatService.js +8 -0
  81. package/dist/esm/services/providers/deepseek/DeepSeekChatService.js.map +1 -0
  82. package/dist/esm/services/providers/deepseek/DeepSeekChatServiceProvider.d.ts +16 -0
  83. package/dist/esm/services/providers/deepseek/DeepSeekChatServiceProvider.d.ts.map +1 -0
  84. package/dist/esm/services/providers/deepseek/DeepSeekChatServiceProvider.js +53 -0
  85. package/dist/esm/services/providers/deepseek/DeepSeekChatServiceProvider.js.map +1 -0
  86. package/dist/esm/services/providers/gemini/GeminiChatService.js +2 -2
  87. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.d.ts.map +1 -1
  88. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.js +3 -16
  89. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.js.map +1 -1
  90. package/dist/esm/services/providers/index.d.ts +3 -1
  91. package/dist/esm/services/providers/index.d.ts.map +1 -1
  92. package/dist/esm/services/providers/index.js +4 -0
  93. package/dist/esm/services/providers/index.js.map +1 -1
  94. package/dist/esm/services/providers/mistral/MistralChatService.d.ts +8 -0
  95. package/dist/esm/services/providers/mistral/MistralChatService.d.ts.map +1 -0
  96. package/dist/esm/services/providers/mistral/MistralChatService.js +8 -0
  97. package/dist/esm/services/providers/mistral/MistralChatService.js.map +1 -0
  98. package/dist/esm/services/providers/mistral/MistralChatServiceProvider.d.ts +17 -0
  99. package/dist/esm/services/providers/mistral/MistralChatServiceProvider.d.ts.map +1 -0
  100. package/dist/esm/services/providers/mistral/MistralChatServiceProvider.js +68 -0
  101. package/dist/esm/services/providers/mistral/MistralChatServiceProvider.js.map +1 -0
  102. package/dist/esm/services/providers/openai/OpenAIChatService.d.ts +2 -0
  103. package/dist/esm/services/providers/openai/OpenAIChatService.d.ts.map +1 -1
  104. package/dist/esm/services/providers/openai/OpenAIChatService.js +43 -5
  105. package/dist/esm/services/providers/openai/OpenAIChatService.js.map +1 -1
  106. package/dist/esm/utils/openaiCompatibleSse.d.ts.map +1 -1
  107. package/dist/esm/utils/openaiCompatibleSse.js +32 -6
  108. package/dist/esm/utils/openaiCompatibleSse.js.map +1 -1
  109. package/dist/umd/aituber-onair-chat.js +1738 -1435
  110. package/dist/umd/aituber-onair-chat.min.js +10 -10
  111. package/package.json +1 -1
@@ -27,21 +27,30 @@ var AITuberOnAirChat = (() => {
27
27
  ChatServiceHttpClient: () => ChatServiceHttpClient,
28
28
  ClaudeChatService: () => ClaudeChatService,
29
29
  ClaudeChatServiceProvider: () => ClaudeChatServiceProvider,
30
+ DEEPSEEK_API_BASE_URL: () => DEEPSEEK_API_BASE_URL,
31
+ DEEPSEEK_DEPRECATED_MODELS: () => DEEPSEEK_DEPRECATED_MODELS,
32
+ DEEPSEEK_SUPPORTED_MODELS: () => DEEPSEEK_SUPPORTED_MODELS,
30
33
  DEFAULT_MAX_TOKENS: () => DEFAULT_MAX_TOKENS,
31
34
  DEFAULT_SUMMARY_PROMPT_TEMPLATE: () => DEFAULT_SUMMARY_PROMPT_TEMPLATE,
32
35
  DEFAULT_VISION_PROMPT: () => DEFAULT_VISION_PROMPT,
36
+ DeepSeekChatService: () => DeepSeekChatService,
37
+ DeepSeekChatServiceProvider: () => DeepSeekChatServiceProvider,
33
38
  EMOTION_TAG_CLEANUP_REGEX: () => EMOTION_TAG_CLEANUP_REGEX,
34
39
  EMOTION_TAG_REGEX: () => EMOTION_TAG_REGEX,
35
40
  ENDPOINT_CLAUDE_API: () => ENDPOINT_CLAUDE_API,
41
+ ENDPOINT_DEEPSEEK_CHAT_COMPLETIONS_API: () => ENDPOINT_DEEPSEEK_CHAT_COMPLETIONS_API,
36
42
  ENDPOINT_GEMINI_API: () => ENDPOINT_GEMINI_API,
37
43
  ENDPOINT_KIMI_CHAT_COMPLETIONS_API: () => ENDPOINT_KIMI_CHAT_COMPLETIONS_API,
44
+ ENDPOINT_MISTRAL_CHAT_COMPLETIONS_API: () => ENDPOINT_MISTRAL_CHAT_COMPLETIONS_API,
38
45
  ENDPOINT_OPENAI_CHAT_COMPLETIONS_API: () => ENDPOINT_OPENAI_CHAT_COMPLETIONS_API,
39
46
  ENDPOINT_OPENAI_RESPONSES_API: () => ENDPOINT_OPENAI_RESPONSES_API,
40
47
  ENDPOINT_OPENROUTER_API: () => ENDPOINT_OPENROUTER_API,
41
48
  ENDPOINT_XAI_CHAT_COMPLETIONS_API: () => ENDPOINT_XAI_CHAT_COMPLETIONS_API,
42
49
  ENDPOINT_ZAI_CHAT_COMPLETIONS_API: () => ENDPOINT_ZAI_CHAT_COMPLETIONS_API,
43
50
  EmotionParser: () => EmotionParser,
51
+ GEMINI_DEPRECATED_MODELS: () => GEMINI_DEPRECATED_MODELS,
44
52
  GEMINI_NANO_MAX_CONTEXT_MESSAGES: () => GEMINI_NANO_MAX_CONTEXT_MESSAGES,
53
+ GEMINI_RECOMMENDED_MODELS: () => GEMINI_RECOMMENDED_MODELS,
45
54
  GEMINI_VISION_SUPPORTED_MODELS: () => GEMINI_VISION_SUPPORTED_MODELS,
46
55
  GPT5_PRESETS: () => GPT5_PRESETS,
47
56
  GPT_5_MODELS: () => GPT_5_MODELS,
@@ -54,6 +63,10 @@ var AITuberOnAirChat = (() => {
54
63
  KimiChatService: () => KimiChatService,
55
64
  KimiChatServiceProvider: () => KimiChatServiceProvider,
56
65
  MAX_TOKENS_BY_LENGTH: () => MAX_TOKENS_BY_LENGTH,
66
+ MISTRAL_API_BASE_URL: () => MISTRAL_API_BASE_URL,
67
+ MISTRAL_REASONING_EFFORT_SUPPORTED_MODELS: () => MISTRAL_REASONING_EFFORT_SUPPORTED_MODELS,
68
+ MISTRAL_SUPPORTED_MODELS: () => MISTRAL_SUPPORTED_MODELS,
69
+ MISTRAL_VISION_SUPPORTED_MODELS: () => MISTRAL_VISION_SUPPORTED_MODELS,
57
70
  MODEL_ANTHROPIC_CLAUDE_3_5_SONNET: () => MODEL_ANTHROPIC_CLAUDE_3_5_SONNET,
58
71
  MODEL_ANTHROPIC_CLAUDE_3_7_SONNET: () => MODEL_ANTHROPIC_CLAUDE_3_7_SONNET,
59
72
  MODEL_ANTHROPIC_CLAUDE_4_5_HAIKU: () => MODEL_ANTHROPIC_CLAUDE_4_5_HAIKU,
@@ -73,12 +86,17 @@ var AITuberOnAirChat = (() => {
73
86
  MODEL_CLAUDE_4_7_OPUS: () => MODEL_CLAUDE_4_7_OPUS,
74
87
  MODEL_CLAUDE_4_OPUS: () => MODEL_CLAUDE_4_OPUS,
75
88
  MODEL_CLAUDE_4_SONNET: () => MODEL_CLAUDE_4_SONNET,
89
+ MODEL_DEEPSEEK_CHAT: () => MODEL_DEEPSEEK_CHAT,
90
+ MODEL_DEEPSEEK_REASONER: () => MODEL_DEEPSEEK_REASONER,
91
+ MODEL_DEEPSEEK_V4_FLASH: () => MODEL_DEEPSEEK_V4_FLASH,
92
+ MODEL_DEEPSEEK_V4_PRO: () => MODEL_DEEPSEEK_V4_PRO,
76
93
  MODEL_GEMINI_2_0_FLASH: () => MODEL_GEMINI_2_0_FLASH,
77
94
  MODEL_GEMINI_2_0_FLASH_LITE: () => MODEL_GEMINI_2_0_FLASH_LITE,
78
95
  MODEL_GEMINI_2_5_FLASH: () => MODEL_GEMINI_2_5_FLASH,
79
96
  MODEL_GEMINI_2_5_FLASH_LITE: () => MODEL_GEMINI_2_5_FLASH_LITE,
80
97
  MODEL_GEMINI_2_5_FLASH_LITE_PREVIEW_06_17: () => MODEL_GEMINI_2_5_FLASH_LITE_PREVIEW_06_17,
81
98
  MODEL_GEMINI_2_5_PRO: () => MODEL_GEMINI_2_5_PRO,
99
+ MODEL_GEMINI_3_1_FLASH_LITE: () => MODEL_GEMINI_3_1_FLASH_LITE,
82
100
  MODEL_GEMINI_3_1_FLASH_LITE_PREVIEW: () => MODEL_GEMINI_3_1_FLASH_LITE_PREVIEW,
83
101
  MODEL_GEMINI_3_1_PRO_PREVIEW: () => MODEL_GEMINI_3_1_PRO_PREVIEW,
84
102
  MODEL_GEMINI_3_FLASH_PREVIEW: () => MODEL_GEMINI_3_FLASH_PREVIEW,
@@ -122,6 +140,12 @@ var AITuberOnAirChat = (() => {
122
140
  MODEL_GROK_4_3: () => MODEL_GROK_4_3,
123
141
  MODEL_KIMI_K2_5: () => MODEL_KIMI_K2_5,
124
142
  MODEL_KIMI_K2_6: () => MODEL_KIMI_K2_6,
143
+ MODEL_MISTRAL_LARGE_2512: () => MODEL_MISTRAL_LARGE_2512,
144
+ MODEL_MISTRAL_LARGE_LATEST: () => MODEL_MISTRAL_LARGE_LATEST,
145
+ MODEL_MISTRAL_MEDIUM_2508: () => MODEL_MISTRAL_MEDIUM_2508,
146
+ MODEL_MISTRAL_MEDIUM_3_5: () => MODEL_MISTRAL_MEDIUM_3_5,
147
+ MODEL_MISTRAL_SMALL_2603: () => MODEL_MISTRAL_SMALL_2603,
148
+ MODEL_MISTRAL_SMALL_LATEST: () => MODEL_MISTRAL_SMALL_LATEST,
125
149
  MODEL_MOONSHOTAI_KIMI_K2_5: () => MODEL_MOONSHOTAI_KIMI_K2_5,
126
150
  MODEL_MOONSHOTAI_KIMI_LATEST: () => MODEL_MOONSHOTAI_KIMI_LATEST,
127
151
  MODEL_O1: () => MODEL_O1,
@@ -142,6 +166,8 @@ var AITuberOnAirChat = (() => {
142
166
  MODEL_ZAI_GLM_4_5_AIR: () => MODEL_ZAI_GLM_4_5_AIR,
143
167
  MODEL_ZAI_GLM_4_5_AIR_FREE: () => MODEL_ZAI_GLM_4_5_AIR_FREE,
144
168
  MODEL_ZAI_GLM_4_7_FLASH: () => MODEL_ZAI_GLM_4_7_FLASH,
169
+ MistralChatService: () => MistralChatService,
170
+ MistralChatServiceProvider: () => MistralChatServiceProvider,
145
171
  OPENROUTER_CREDITS_THRESHOLD: () => OPENROUTER_CREDITS_THRESHOLD,
146
172
  OPENROUTER_FREE_DAILY_LIMIT_HIGH_CREDITS: () => OPENROUTER_FREE_DAILY_LIMIT_HIGH_CREDITS,
147
173
  OPENROUTER_FREE_DAILY_LIMIT_LOW_CREDITS: () => OPENROUTER_FREE_DAILY_LIMIT_LOW_CREDITS,
@@ -171,6 +197,9 @@ var AITuberOnAirChat = (() => {
171
197
  installGASFetch: () => installGASFetch,
172
198
  isGPT5Model: () => isGPT5Model,
173
199
  isKimiVisionModel: () => isKimiVisionModel,
200
+ isMistralReasoningEffort: () => isMistralReasoningEffort,
201
+ isMistralReasoningEffortModel: () => isMistralReasoningEffortModel,
202
+ isMistralVisionModel: () => isMistralVisionModel,
174
203
  isOpenRouterFreeModel: () => isOpenRouterFreeModel,
175
204
  isOpenRouterVisionModel: () => isOpenRouterVisionModel,
176
205
  isResponsesOnlyGPT5Model: () => isResponsesOnlyGPT5Model,
@@ -268,6 +297,7 @@ var AITuberOnAirChat = (() => {
268
297
  var MODEL_GEMMA_4_31B_IT = "gemma-4-31b-it";
269
298
  var MODEL_GEMMA_4_26B_A4B_IT = "gemma-4-26b-a4b-it";
270
299
  var MODEL_GEMINI_3_1_PRO_PREVIEW = "gemini-3.1-pro-preview";
300
+ var MODEL_GEMINI_3_1_FLASH_LITE = "gemini-3.1-flash-lite";
271
301
  var MODEL_GEMINI_3_1_FLASH_LITE_PREVIEW = "gemini-3.1-flash-lite-preview";
272
302
  var MODEL_GEMINI_3_PRO_PREVIEW = "gemini-3-pro-preview";
273
303
  var MODEL_GEMINI_3_FLASH_PREVIEW = "gemini-3-flash-preview";
@@ -277,20 +307,27 @@ var AITuberOnAirChat = (() => {
277
307
  var MODEL_GEMINI_2_5_FLASH_LITE_PREVIEW_06_17 = "gemini-2.5-flash-lite-preview-06-17";
278
308
  var MODEL_GEMINI_2_0_FLASH = "gemini-2.0-flash";
279
309
  var MODEL_GEMINI_2_0_FLASH_LITE = "gemini-2.0-flash-lite";
280
- var GEMINI_VISION_SUPPORTED_MODELS = [
281
- MODEL_GEMMA_4_31B_IT,
282
- MODEL_GEMMA_4_26B_A4B_IT,
310
+ var GEMINI_RECOMMENDED_MODELS = [
311
+ MODEL_GEMINI_3_1_FLASH_LITE,
283
312
  MODEL_GEMINI_3_1_PRO_PREVIEW,
284
- MODEL_GEMINI_3_1_FLASH_LITE_PREVIEW,
285
- MODEL_GEMINI_3_PRO_PREVIEW,
286
313
  MODEL_GEMINI_3_FLASH_PREVIEW,
287
314
  MODEL_GEMINI_2_5_PRO,
288
315
  MODEL_GEMINI_2_5_FLASH,
289
316
  MODEL_GEMINI_2_5_FLASH_LITE,
317
+ MODEL_GEMMA_4_31B_IT,
318
+ MODEL_GEMMA_4_26B_A4B_IT
319
+ ];
320
+ var GEMINI_DEPRECATED_MODELS = [
321
+ MODEL_GEMINI_3_1_FLASH_LITE_PREVIEW,
322
+ MODEL_GEMINI_3_PRO_PREVIEW,
290
323
  MODEL_GEMINI_2_5_FLASH_LITE_PREVIEW_06_17,
291
324
  MODEL_GEMINI_2_0_FLASH,
292
325
  MODEL_GEMINI_2_0_FLASH_LITE
293
326
  ];
327
+ var GEMINI_VISION_SUPPORTED_MODELS = [
328
+ ...GEMINI_RECOMMENDED_MODELS,
329
+ ...GEMINI_DEPRECATED_MODELS
330
+ ];
294
331
 
295
332
  // src/constants/claude.ts
296
333
  var ENDPOINT_CLAUDE_API = "https://api.anthropic.com/v1/messages";
@@ -443,6 +480,61 @@ var AITuberOnAirChat = (() => {
443
480
  return KIMI_VISION_SUPPORTED_MODELS.includes(model);
444
481
  }
445
482
 
483
+ // src/constants/deepseek.ts
484
+ var DEEPSEEK_API_BASE_URL = "https://api.deepseek.com";
485
+ var ENDPOINT_DEEPSEEK_CHAT_COMPLETIONS_API = `${DEEPSEEK_API_BASE_URL}/chat/completions`;
486
+ var MODEL_DEEPSEEK_V4_FLASH = "deepseek-v4-flash";
487
+ var MODEL_DEEPSEEK_V4_PRO = "deepseek-v4-pro";
488
+ var MODEL_DEEPSEEK_CHAT = "deepseek-chat";
489
+ var MODEL_DEEPSEEK_REASONER = "deepseek-reasoner";
490
+ var DEEPSEEK_SUPPORTED_MODELS = [
491
+ MODEL_DEEPSEEK_V4_FLASH,
492
+ MODEL_DEEPSEEK_V4_PRO
493
+ ];
494
+ var DEEPSEEK_DEPRECATED_MODELS = [
495
+ MODEL_DEEPSEEK_CHAT,
496
+ MODEL_DEEPSEEK_REASONER
497
+ ];
498
+
499
+ // src/constants/mistral.ts
500
+ var MISTRAL_API_BASE_URL = "https://api.mistral.ai/v1";
501
+ var ENDPOINT_MISTRAL_CHAT_COMPLETIONS_API = `${MISTRAL_API_BASE_URL}/chat/completions`;
502
+ var MODEL_MISTRAL_SMALL_LATEST = "mistral-small-latest";
503
+ var MODEL_MISTRAL_SMALL_2603 = "mistral-small-2603";
504
+ var MODEL_MISTRAL_MEDIUM_3_5 = "mistral-medium-3-5";
505
+ var MODEL_MISTRAL_MEDIUM_2508 = "mistral-medium-2508";
506
+ var MODEL_MISTRAL_LARGE_LATEST = "mistral-large-latest";
507
+ var MODEL_MISTRAL_LARGE_2512 = "mistral-large-2512";
508
+ var MISTRAL_SUPPORTED_MODELS = [
509
+ MODEL_MISTRAL_SMALL_LATEST,
510
+ MODEL_MISTRAL_MEDIUM_3_5,
511
+ MODEL_MISTRAL_LARGE_LATEST,
512
+ MODEL_MISTRAL_LARGE_2512,
513
+ MODEL_MISTRAL_SMALL_2603,
514
+ MODEL_MISTRAL_MEDIUM_2508
515
+ ];
516
+ var MISTRAL_REASONING_EFFORT_SUPPORTED_MODELS = [
517
+ MODEL_MISTRAL_SMALL_LATEST,
518
+ MODEL_MISTRAL_MEDIUM_3_5
519
+ ];
520
+ var MISTRAL_VISION_SUPPORTED_MODELS = [
521
+ MODEL_MISTRAL_SMALL_LATEST,
522
+ MODEL_MISTRAL_SMALL_2603,
523
+ MODEL_MISTRAL_MEDIUM_3_5,
524
+ MODEL_MISTRAL_MEDIUM_2508,
525
+ MODEL_MISTRAL_LARGE_LATEST,
526
+ MODEL_MISTRAL_LARGE_2512
527
+ ];
528
+ function isMistralReasoningEffortModel(model) {
529
+ return MISTRAL_REASONING_EFFORT_SUPPORTED_MODELS.includes(model);
530
+ }
531
+ function isMistralReasoningEffort(effort) {
532
+ return effort === "none" || effort === "high";
533
+ }
534
+ function isMistralVisionModel(model) {
535
+ return MISTRAL_VISION_SUPPORTED_MODELS.includes(model);
536
+ }
537
+
446
538
  // src/constants/chat.ts
447
539
  var CHAT_RESPONSE_LENGTH = {
448
540
  VERY_SHORT: "veryShort",
@@ -756,6 +848,23 @@ If it's in another language, summarize in that language.
756
848
  throw error;
757
849
  }
758
850
  };
851
+ var extractTextContent = (content) => {
852
+ if (typeof content === "string") {
853
+ return content;
854
+ }
855
+ if (!Array.isArray(content)) {
856
+ return "";
857
+ }
858
+ return content.map((chunk) => {
859
+ if (typeof chunk === "string") {
860
+ return chunk;
861
+ }
862
+ if (chunk && typeof chunk === "object" && chunk.type === "text" && typeof chunk.text === "string") {
863
+ return chunk.text;
864
+ }
865
+ return "";
866
+ }).join("");
867
+ };
759
868
  var forEachSsePayload = async (res, onPayload) => {
760
869
  const reader = res.body?.getReader();
761
870
  if (!reader) {
@@ -788,7 +897,7 @@ If it's in another language, summarize in that language.
788
897
  await forEachSsePayload(res, (payload) => {
789
898
  const json = parseJsonPayload(payload, options.onJsonError);
790
899
  if (!json) return;
791
- const content = json.choices?.[0]?.delta?.content || "";
900
+ const content = extractTextContent(json.choices?.[0]?.delta?.content);
792
901
  if (content) {
793
902
  onPartial(content);
794
903
  full += content;
@@ -813,9 +922,10 @@ If it's in another language, summarize in that language.
813
922
  usage = json.usage;
814
923
  }
815
924
  const delta = choice?.delta;
816
- if (delta?.content) {
817
- onPartial(delta.content);
818
- appendTextBlock(textBlocks, delta.content);
925
+ const content = extractTextContent(delta?.content);
926
+ if (content) {
927
+ onPartial(content);
928
+ appendTextBlock(textBlocks, content);
819
929
  }
820
930
  if (delta?.tool_calls) {
821
931
  delta.tool_calls.forEach((c) => {
@@ -856,8 +966,11 @@ If it's in another language, summarize in that language.
856
966
  input: JSON.parse(c.function?.arguments || "{}")
857
967
  })
858
968
  );
859
- } else if (choice?.message?.content) {
860
- blocks.push({ type: "text", text: choice.message.content });
969
+ } else {
970
+ const content = extractTextContent(choice?.message?.content);
971
+ if (content) {
972
+ blocks.push({ type: "text", text: content });
973
+ }
861
974
  }
862
975
  return {
863
976
  blocks,
@@ -1660,171 +1773,210 @@ If it's in another language, summarize in that language.
1660
1773
  }
1661
1774
  };
1662
1775
 
1663
- // src/utils/mcpSchemaFetcher.ts
1664
- var MCPSchemaFetcher = class {
1665
- /**
1666
- * Fetch tool schemas from MCP server
1667
- * @param serverConfig MCP server configuration
1668
- * @returns Array of tool definitions
1669
- */
1670
- static async fetchToolSchemas(serverConfig) {
1671
- try {
1672
- const headers = {
1673
- "Content-Type": "application/json"
1674
- };
1675
- if (serverConfig.authorization_token) {
1676
- headers["Authorization"] = `Bearer ${serverConfig.authorization_token}`;
1677
- }
1678
- const response = await ChatServiceHttpClient.post(
1679
- `${serverConfig.url}/tools`,
1680
- {},
1681
- headers
1682
- );
1683
- const toolsData = await response.json();
1684
- if (Array.isArray(toolsData.tools)) {
1685
- return toolsData.tools.map((tool) => ({
1686
- name: `mcp_${serverConfig.name}_${tool.name}`,
1687
- description: tool.description || `Tool from ${serverConfig.name} MCP server`,
1688
- parameters: tool.inputSchema || {
1689
- type: "object",
1690
- properties: {},
1691
- required: []
1692
- }
1693
- }));
1694
- }
1695
- return [
1696
- {
1697
- name: `mcp_${serverConfig.name}_search`,
1698
- description: `Search using ${serverConfig.name} MCP server`,
1699
- parameters: {
1700
- type: "object",
1701
- properties: {
1702
- query: {
1703
- type: "string",
1704
- description: "Search query"
1776
+ // src/services/providers/openai/responsesParser.ts
1777
+ async function parseOpenAIResponsesStream(res, onPartial) {
1778
+ const reader = res.body.getReader();
1779
+ const dec = new TextDecoder();
1780
+ const textBlocks = [];
1781
+ const toolCallsMap = /* @__PURE__ */ new Map();
1782
+ let responseStatus;
1783
+ let incompleteDetails;
1784
+ let usage;
1785
+ let buf = "";
1786
+ while (true) {
1787
+ const { done, value } = await reader.read();
1788
+ if (done) break;
1789
+ buf += dec.decode(value, { stream: true });
1790
+ let eventType = "";
1791
+ let eventData = "";
1792
+ const lines = buf.split("\n");
1793
+ buf = lines.pop() || "";
1794
+ for (let i = 0; i < lines.length; i++) {
1795
+ const line = lines[i].trim();
1796
+ if (line.startsWith("event:")) {
1797
+ eventType = line.slice(6).trim();
1798
+ } else if (line.startsWith("data:")) {
1799
+ eventData = line.slice(5).trim();
1800
+ } else if (line === "" && eventType && eventData) {
1801
+ try {
1802
+ const json = JSON.parse(eventData);
1803
+ handleResponsesSSEEvent(
1804
+ eventType,
1805
+ json,
1806
+ onPartial,
1807
+ textBlocks,
1808
+ toolCallsMap,
1809
+ (metadata) => {
1810
+ if (metadata.responseStatus !== void 0) {
1811
+ responseStatus = metadata.responseStatus;
1705
1812
  }
1706
- },
1707
- required: ["query"]
1708
- }
1709
- }
1710
- ];
1711
- } catch (error) {
1712
- console.warn(
1713
- `Failed to fetch MCP schemas from ${serverConfig.name}:`,
1714
- error
1715
- );
1716
- return [
1717
- {
1718
- name: `mcp_${serverConfig.name}_search`,
1719
- description: `Search using ${serverConfig.name} MCP server (schema fetch failed)`,
1720
- parameters: {
1721
- type: "object",
1722
- properties: {
1723
- query: {
1724
- type: "string",
1725
- description: "Search query"
1813
+ if (metadata.incompleteDetails !== void 0) {
1814
+ incompleteDetails = metadata.incompleteDetails;
1726
1815
  }
1727
- },
1728
- required: ["query"]
1729
- }
1816
+ if (metadata.usage !== void 0) {
1817
+ usage = metadata.usage;
1818
+ }
1819
+ }
1820
+ );
1821
+ } catch {
1822
+ console.warn("Failed to parse SSE data:", eventData);
1730
1823
  }
1731
- ];
1732
- }
1733
- }
1734
- /**
1735
- * Fetch all tool schemas from multiple MCP servers
1736
- * @param mcpServers Array of MCP server configurations
1737
- * @returns Array of all tool definitions
1738
- */
1739
- static async fetchAllToolSchemas(mcpServers) {
1740
- const allSchemas = [];
1741
- for (const server of mcpServers) {
1742
- try {
1743
- const schemas = await this.fetchToolSchemas(server);
1744
- allSchemas.push(...schemas);
1745
- } catch (error) {
1746
- console.error(`Failed to fetch schemas from ${server.name}:`, error);
1824
+ eventType = "";
1825
+ eventData = "";
1747
1826
  }
1748
1827
  }
1749
- return allSchemas;
1750
1828
  }
1751
- };
1752
-
1753
- // src/services/providers/gemini/GeminiChatService.ts
1754
- var GeminiChatService = class {
1755
- /**
1756
- * Constructor
1757
- * @param apiKey Google API key
1758
- * @param model Name of the model to use
1759
- * @param visionModel Name of the vision model
1760
- * @param tools Array of tool definitions
1761
- * @param mcpServers Array of MCP server configurations
1762
- */
1763
- constructor(apiKey, model = MODEL_GEMINI_2_0_FLASH_LITE, visionModel = MODEL_GEMINI_2_0_FLASH_LITE, tools = [], mcpServers = [], responseLength) {
1764
- /** Provider name */
1765
- this.provider = "gemini";
1766
- this.mcpToolSchemas = [];
1767
- this.mcpSchemasInitialized = false;
1768
- /** id(OpenAI) name(Gemini) mapping */
1769
- this.callIdMap = /* @__PURE__ */ new Map();
1770
- this.apiKey = apiKey;
1771
- this.model = model;
1772
- this.responseLength = responseLength;
1773
- if (!GEMINI_VISION_SUPPORTED_MODELS.includes(visionModel)) {
1774
- throw new Error(
1775
- `Model ${visionModel} does not support vision capabilities.`
1776
- );
1829
+ const toolBlocks = Array.from(toolCallsMap.values()).map(
1830
+ (tool) => ({
1831
+ type: "tool_use",
1832
+ id: tool.id,
1833
+ name: tool.name,
1834
+ input: tool.input || {}
1835
+ })
1836
+ );
1837
+ return {
1838
+ blocks: [...textBlocks, ...toolBlocks],
1839
+ stop_reason: toolBlocks.length ? "tool_use" : "end",
1840
+ truncated: responseStatus === "incomplete",
1841
+ response_status: responseStatus,
1842
+ incomplete_details: incompleteDetails,
1843
+ usage
1844
+ };
1845
+ }
1846
+ function handleResponsesSSEEvent(eventType, data, onPartial, textBlocks, toolCallsMap, onMetadata) {
1847
+ switch (eventType) {
1848
+ case "response.output_item.added":
1849
+ if (data.item?.type === "message" && Array.isArray(data.item.content)) {
1850
+ data.item.content.forEach((c) => {
1851
+ if (c.type === "output_text" && c.text) {
1852
+ onPartial(c.text);
1853
+ StreamTextAccumulator.append(textBlocks, c.text);
1854
+ }
1855
+ });
1856
+ } else if (data.item?.type === "function_call") {
1857
+ toolCallsMap.set(data.item.id, {
1858
+ id: data.item.id,
1859
+ name: data.item.name,
1860
+ input: data.item.arguments ? JSON.parse(data.item.arguments) : {}
1861
+ });
1862
+ }
1863
+ break;
1864
+ case "response.content_part.added":
1865
+ if (data.part?.type === "output_text" && typeof data.part.text === "string") {
1866
+ onPartial(data.part.text);
1867
+ StreamTextAccumulator.append(textBlocks, data.part.text);
1868
+ }
1869
+ break;
1870
+ case "response.output_text.delta":
1871
+ case "response.content_part.delta": {
1872
+ const deltaText = typeof data.delta === "string" ? data.delta : data.delta?.text ?? "";
1873
+ if (deltaText) {
1874
+ onPartial(deltaText);
1875
+ StreamTextAccumulator.append(textBlocks, deltaText);
1876
+ }
1877
+ break;
1777
1878
  }
1778
- this.visionModel = visionModel;
1779
- this.tools = tools;
1780
- this.mcpServers = mcpServers;
1879
+ case "response.output_text.done":
1880
+ case "response.content_part.done":
1881
+ case "response.reasoning.started":
1882
+ case "response.reasoning.delta":
1883
+ case "response.reasoning.done":
1884
+ break;
1885
+ case "response.completed":
1886
+ onMetadata(extractResponsesMetadata(data, "completed"));
1887
+ break;
1888
+ case "response.incomplete":
1889
+ onMetadata(extractResponsesMetadata(data, "incomplete"));
1890
+ break;
1891
+ default:
1892
+ break;
1781
1893
  }
1782
- /* ────────────────────────────────── */
1783
- /* Utilities */
1784
- /* ────────────────────────────────── */
1785
- safeJsonParse(str) {
1786
- try {
1787
- return JSON.parse(str);
1788
- } catch {
1789
- return str;
1790
- }
1791
- }
1792
- normalizeToolResult(val) {
1793
- if (val === null) return { content: null };
1794
- if (typeof val === "object") return val;
1795
- return { content: val };
1796
- }
1797
- isGemma4Model(model) {
1798
- return /^gemma-4-/.test(model);
1799
- }
1800
- shouldExposeTextPart(part, model) {
1801
- if (!part.text) return false;
1802
- if (this.isGemma4Model(model) && part.thought === true) {
1803
- return false;
1804
- }
1805
- return true;
1894
+ }
1895
+ function extractResponsesMetadata(data, fallbackStatus) {
1896
+ const response = data?.response ?? data;
1897
+ return {
1898
+ responseStatus: response?.status ?? fallbackStatus,
1899
+ incompleteDetails: response?.incomplete_details ?? null,
1900
+ usage: response?.usage
1901
+ };
1902
+ }
1903
+ function parseOpenAIResponsesOneShot(data) {
1904
+ const blocks = [];
1905
+ if (data.output && Array.isArray(data.output)) {
1906
+ data.output.forEach((outputItem) => {
1907
+ if (outputItem.type === "message" && outputItem.content) {
1908
+ outputItem.content.forEach((content) => {
1909
+ if (content.type === "output_text" && content.text) {
1910
+ blocks.push({ type: "text", text: content.text });
1911
+ }
1912
+ });
1913
+ }
1914
+ if (outputItem.type === "function_call") {
1915
+ blocks.push({
1916
+ type: "tool_use",
1917
+ id: outputItem.id,
1918
+ name: outputItem.name,
1919
+ input: outputItem.arguments ? JSON.parse(outputItem.arguments) : {}
1920
+ });
1921
+ }
1922
+ });
1806
1923
  }
1924
+ return {
1925
+ blocks,
1926
+ stop_reason: blocks.some((b) => b.type === "tool_use") ? "tool_use" : "end",
1927
+ truncated: data?.status === "incomplete",
1928
+ response_status: data?.status,
1929
+ incomplete_details: data?.incomplete_details ?? null,
1930
+ usage: data?.usage
1931
+ };
1932
+ }
1933
+
1934
+ // src/services/providers/openai/OpenAIChatService.ts
1935
+ var GPT5_RESPONSE_LENGTH_MIN_TOKENS = {
1936
+ [CHAT_RESPONSE_LENGTH.VERY_SHORT]: 800,
1937
+ [CHAT_RESPONSE_LENGTH.SHORT]: 1200,
1938
+ [CHAT_RESPONSE_LENGTH.MEDIUM]: 2e3,
1939
+ [CHAT_RESPONSE_LENGTH.LONG]: 3e3,
1940
+ [CHAT_RESPONSE_LENGTH.VERY_LONG]: 8e3,
1941
+ [CHAT_RESPONSE_LENGTH.DEEP]: 25e3
1942
+ };
1943
+ var GPT5_REASONING_MIN_TOKENS = {
1944
+ none: 1200,
1945
+ minimal: 1600,
1946
+ low: 2500,
1947
+ medium: 4e3,
1948
+ high: 8e3,
1949
+ xhigh: 12e3
1950
+ };
1951
+ var OPENAI_COMPATIBLE_CHAT_COMPLETIONS_PROVIDERS = /* @__PURE__ */ new Set([
1952
+ "openai-compatible",
1953
+ "deepseek",
1954
+ "mistral"
1955
+ ]);
1956
+ var OpenAIChatService = class {
1807
1957
  /**
1808
- * camelCase → snake_case conversion (v1beta)
1958
+ * Constructor
1959
+ * @param apiKey OpenAI API key
1960
+ * @param model Name of the model to use
1961
+ * @param visionModel Name of the vision model
1809
1962
  */
1810
- adaptKeysForApi(obj) {
1811
- const map = {
1812
- toolConfig: "tool_config",
1813
- functionCallingConfig: "function_calling_config",
1814
- functionDeclarations: "function_declarations",
1815
- functionCall: "function_call",
1816
- functionResponse: "function_response"
1817
- };
1818
- if (Array.isArray(obj)) return obj.map((v) => this.adaptKeysForApi(v));
1819
- if (obj && typeof obj === "object") {
1820
- return Object.fromEntries(
1821
- Object.entries(obj).map(([k, v]) => [
1822
- map[k] ?? k,
1823
- this.adaptKeysForApi(v)
1824
- ])
1963
+ constructor(apiKey, model = MODEL_GPT_4O_MINI, visionModel = MODEL_GPT_4O_MINI, tools, endpoint = ENDPOINT_OPENAI_CHAT_COMPLETIONS_API, mcpServers = [], responseLength, verbosity, reasoning_effort, enableReasoningSummary = false, provider = "openai", validateVisionModel = true) {
1964
+ this.provider = provider;
1965
+ this.apiKey = apiKey;
1966
+ this.model = model;
1967
+ this.tools = tools || [];
1968
+ this.endpoint = endpoint;
1969
+ this.mcpServers = mcpServers;
1970
+ this.responseLength = responseLength;
1971
+ this.verbosity = verbosity;
1972
+ this.reasoning_effort = reasoning_effort;
1973
+ this.enableReasoningSummary = enableReasoningSummary;
1974
+ if (validateVisionModel && !VISION_SUPPORTED_MODELS.includes(visionModel)) {
1975
+ throw new Error(
1976
+ `Model ${visionModel} does not support vision capabilities.`
1825
1977
  );
1826
1978
  }
1827
- return obj;
1979
+ this.visionModel = visionModel;
1828
1980
  }
1829
1981
  /**
1830
1982
  * Get the current model name
@@ -1841,1262 +1993,1383 @@ If it's in another language, summarize in that language.
1841
1993
  return this.visionModel;
1842
1994
  }
1843
1995
  /**
1844
- * Get configured MCP servers
1845
- * @returns Array of MCP server configurations
1846
- */
1847
- getMCPServers() {
1848
- return this.mcpServers;
1849
- }
1850
- /**
1851
- * Add MCP server configuration
1852
- * @param serverConfig MCP server configuration
1853
- */
1854
- addMCPServer(serverConfig) {
1855
- this.mcpServers.push(serverConfig);
1856
- this.mcpSchemasInitialized = false;
1857
- }
1858
- /**
1859
- * Remove MCP server by name
1860
- * @param serverName Name of the server to remove
1861
- */
1862
- removeMCPServer(serverName) {
1863
- this.mcpServers = this.mcpServers.filter(
1864
- (server) => server.name !== serverName
1865
- );
1866
- this.mcpSchemasInitialized = false;
1867
- }
1868
- /**
1869
- * Check if MCP servers are configured
1870
- * @returns True if MCP servers are configured
1871
- */
1872
- hasMCPServers() {
1873
- return this.mcpServers.length > 0;
1874
- }
1875
- /**
1876
- * Initialize MCP tool schemas by fetching from servers
1877
- * @private
1996
+ * Process chat messages
1997
+ * @param messages Array of messages to send
1998
+ * @param onPartialResponse Callback to receive each part of streaming response
1999
+ * @param onCompleteResponse Callback to execute when response is complete
1878
2000
  */
1879
- async initializeMCPSchemas() {
1880
- if (this.mcpSchemasInitialized || this.mcpServers.length === 0) {
1881
- return;
1882
- }
1883
- try {
1884
- const timeoutPromise = new Promise(
1885
- (_, reject) => setTimeout(() => reject(new Error("MCP schema fetch timeout")), 5e3)
1886
- );
1887
- const schemasPromise = MCPSchemaFetcher.fetchAllToolSchemas(
1888
- this.mcpServers
1889
- );
1890
- this.mcpToolSchemas = await Promise.race([
1891
- schemasPromise,
1892
- timeoutPromise
1893
- ]);
1894
- this.mcpSchemasInitialized = true;
1895
- } catch (error) {
1896
- console.warn("Failed to initialize MCP schemas, using fallback:", error);
1897
- this.mcpToolSchemas = this.mcpServers.map((server) => ({
1898
- name: `mcp_${server.name}_search`,
1899
- description: `Search using ${server.name} MCP server (fallback)`,
1900
- parameters: {
1901
- type: "object",
1902
- properties: {
1903
- query: {
1904
- type: "string",
1905
- description: "Search query"
1906
- }
1907
- },
1908
- required: ["query"]
2001
+ async processChat(messages, onPartialResponse, onCompleteResponse) {
2002
+ await processChatWithOptionalTools({
2003
+ hasTools: this.tools.length > 0,
2004
+ runWithoutTools: async () => {
2005
+ const res = await this.callOpenAI(messages, this.model, true);
2006
+ const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2007
+ try {
2008
+ if (isResponsesAPI) {
2009
+ const result = await parseOpenAIResponsesStream(
2010
+ res,
2011
+ onPartialResponse
2012
+ );
2013
+ return StreamTextAccumulator.getFullText(result.blocks);
2014
+ }
2015
+ return this.handleStream(res, onPartialResponse);
2016
+ } catch (error) {
2017
+ console.error("[processChat] Error in streaming/completion:", error);
2018
+ throw error;
1909
2019
  }
1910
- }));
1911
- this.mcpSchemasInitialized = true;
1912
- }
2020
+ },
2021
+ runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
2022
+ onCompleteResponse,
2023
+ toolErrorMessage: "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
2024
+ });
1913
2025
  }
1914
2026
  /**
1915
- * Process chat messages
1916
- * @param messages Array of messages to send
2027
+ * Process chat messages with images
2028
+ * @param messages Array of messages to send (including images)
1917
2029
  * @param onPartialResponse Callback to receive each part of streaming response
1918
2030
  * @param onCompleteResponse Callback to execute when response is complete
2031
+ * @throws Error if the selected model doesn't support vision
1919
2032
  */
1920
- async processChat(messages, onPartialResponse, onCompleteResponse) {
2033
+ async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
1921
2034
  try {
1922
2035
  await processChatWithOptionalTools({
1923
- hasTools: this.tools.length > 0 || this.mcpServers.length > 0,
2036
+ hasTools: this.tools.length > 0,
1924
2037
  runWithoutTools: async () => {
1925
- const res = await this.callGemini(messages, this.model, true);
1926
- const { blocks } = await this.parseStream(
1927
- res,
1928
- onPartialResponse,
1929
- this.model
1930
- );
1931
- return StreamTextAccumulator.getFullText(blocks);
2038
+ const res = await this.callOpenAI(messages, this.visionModel, true);
2039
+ const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2040
+ try {
2041
+ if (isResponsesAPI) {
2042
+ const result = await parseOpenAIResponsesStream(
2043
+ res,
2044
+ onPartialResponse
2045
+ );
2046
+ return StreamTextAccumulator.getFullText(result.blocks);
2047
+ }
2048
+ return this.handleStream(res, onPartialResponse);
2049
+ } catch (streamError) {
2050
+ console.error(
2051
+ "[processVisionChat] Error in streaming/completion:",
2052
+ streamError
2053
+ );
2054
+ throw streamError;
2055
+ }
1932
2056
  },
1933
- runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
2057
+ runWithTools: () => this.visionChatOnce(messages, true, onPartialResponse),
1934
2058
  onCompleteResponse,
1935
- toolErrorMessage: "Received functionCall. Use chatOnce() loop when tools are enabled."
2059
+ toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
1936
2060
  });
1937
- } catch (err) {
1938
- console.error("Error in processChat:", err);
1939
- throw err;
2061
+ } catch (error) {
2062
+ console.error("Error in processVisionChat:", error);
2063
+ throw error;
1940
2064
  }
1941
2065
  }
1942
- async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
1943
- try {
1944
- await processChatWithOptionalTools({
1945
- hasTools: this.tools.length > 0 || this.mcpServers.length > 0,
1946
- runWithoutTools: async () => {
1947
- const res = await this.callGemini(messages, this.visionModel, true);
1948
- const { blocks } = await this.parseStream(
1949
- res,
1950
- onPartialResponse,
1951
- this.visionModel
1952
- );
1953
- return StreamTextAccumulator.getFullText(blocks);
1954
- },
1955
- runWithTools: () => this.visionChatOnce(messages),
1956
- onToolBlocks: (blocks) => {
1957
- blocks.filter(
1958
- (b) => b.type === "text"
1959
- ).forEach((b) => onPartialResponse(b.text));
1960
- },
1961
- onCompleteResponse,
1962
- toolErrorMessage: "Received functionCall. Use visionChatOnce() loop when tools are enabled."
1963
- });
1964
- } catch (err) {
1965
- console.error("Error in processVisionChat:", err);
1966
- throw err;
2066
+ /**
2067
+ * Process chat messages with tools (text only)
2068
+ * @param messages Array of messages to send
2069
+ * @param stream Whether to use streaming
2070
+ * @param onPartialResponse Callback for partial responses
2071
+ * @param maxTokens Maximum tokens for response (optional)
2072
+ * @returns Tool chat completion
2073
+ */
2074
+ async chatOnce(messages, stream = true, onPartialResponse = () => {
2075
+ }, maxTokens) {
2076
+ const res = await this.callOpenAI(messages, this.model, stream, maxTokens);
2077
+ return this.parseResponse(res, stream, onPartialResponse);
2078
+ }
2079
+ /**
2080
+ * Process vision chat messages with tools
2081
+ * @param messages Array of messages to send (including images)
2082
+ * @param stream Whether to use streaming
2083
+ * @param onPartialResponse Callback for partial responses
2084
+ * @param maxTokens Maximum tokens for response (optional)
2085
+ * @returns Tool chat completion
2086
+ */
2087
+ async visionChatOnce(messages, stream = false, onPartialResponse = () => {
2088
+ }, maxTokens) {
2089
+ const res = await this.callOpenAI(
2090
+ messages,
2091
+ this.visionModel,
2092
+ stream,
2093
+ maxTokens
2094
+ );
2095
+ return this.parseResponse(res, stream, onPartialResponse);
2096
+ }
2097
+ /**
2098
+ * Parse response based on endpoint type
2099
+ */
2100
+ async parseResponse(res, stream, onPartialResponse) {
2101
+ const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2102
+ if (isResponsesAPI) {
2103
+ return stream ? parseOpenAIResponsesStream(res, onPartialResponse) : parseOpenAIResponsesOneShot(await res.json());
1967
2104
  }
2105
+ return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
1968
2106
  }
1969
- /* ────────────────────────────────── */
1970
- /* OpenAI Gemini conversion */
1971
- /* ────────────────────────────────── */
1972
- convertMessagesToGeminiFormat(messages) {
1973
- const gemini = [];
1974
- let currentRole = null;
1975
- let currentParts = [];
1976
- const pushCurrent = () => {
1977
- if (currentRole && currentParts.length) {
1978
- gemini.push({ role: currentRole, parts: [...currentParts] });
1979
- currentParts = [];
1980
- }
1981
- };
1982
- for (const msg of messages) {
1983
- const role = this.mapRoleToGemini(msg.role);
1984
- if (msg.tool_calls) {
1985
- pushCurrent();
1986
- for (const call of msg.tool_calls) {
1987
- this.callIdMap.set(call.id, call.function.name);
1988
- gemini.push({
1989
- role: "model",
1990
- parts: [
1991
- {
1992
- functionCall: {
1993
- name: call.function.name,
1994
- args: JSON.parse(call.function.arguments || "{}")
1995
- }
1996
- }
1997
- ]
1998
- });
1999
- }
2000
- continue;
2001
- }
2002
- if (msg.role === "tool") {
2003
- pushCurrent();
2004
- const funcName = msg.name ?? this.callIdMap.get(msg.tool_call_id) ?? "result";
2005
- gemini.push({
2006
- role: "user",
2007
- parts: [
2008
- {
2009
- functionResponse: {
2010
- name: funcName,
2011
- response: this.normalizeToolResult(
2012
- this.safeJsonParse(msg.content)
2013
- )
2014
- }
2015
- }
2016
- ]
2017
- });
2018
- continue;
2019
- }
2020
- if (role !== currentRole) pushCurrent();
2021
- currentRole = role;
2022
- currentParts.push({ text: msg.content });
2107
+ async callOpenAI(messages, model, stream = false, maxTokens) {
2108
+ const body = this.buildRequestBody(messages, model, stream, maxTokens);
2109
+ const headers = {};
2110
+ const shouldSendAuthorization = this.provider !== "openai-compatible" || this.apiKey.trim() !== "";
2111
+ if (shouldSendAuthorization) {
2112
+ headers.Authorization = `Bearer ${this.apiKey}`;
2023
2113
  }
2024
- pushCurrent();
2025
- return gemini;
2114
+ const res = await ChatServiceHttpClient.post(this.endpoint, body, headers);
2115
+ return res;
2026
2116
  }
2027
- /* ────────────────────────────────── */
2028
- /* HTTP call */
2029
- /* ────────────────────────────────── */
2030
- async callGemini(messages, model, stream = false, maxTokens) {
2031
- const hasVision = messages.some(
2032
- (m) => Array.isArray(m.content) && m.content.some(
2033
- (b) => b?.type === "image_url" || b?.inlineData
2034
- )
2035
- );
2036
- const contents = hasVision ? await this.convertVisionMessagesToGeminiFormat(
2037
- messages
2038
- ) : this.convertMessagesToGeminiFormat(messages);
2117
+ /**
2118
+ * Build request body based on the endpoint type
2119
+ */
2120
+ buildRequestBody(messages, model, stream, maxTokens) {
2121
+ const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2122
+ this.validateMCPCompatibility();
2039
2123
  const body = {
2040
- contents,
2041
- generationConfig: {
2042
- maxOutputTokens: maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength)
2043
- }
2124
+ model,
2125
+ stream
2044
2126
  };
2045
- if (this.isGemma4Model(model)) {
2046
- body.generationConfig.thinkingConfig = {
2047
- includeThoughts: false,
2048
- thinkingLevel: "minimal"
2049
- };
2050
- }
2051
- const allToolDeclarations = [];
2052
- if (this.tools.length > 0) {
2053
- allToolDeclarations.push(
2054
- ...this.tools.map((t) => ({
2055
- name: t.name,
2056
- description: t.description,
2057
- parameters: t.parameters
2058
- }))
2059
- );
2060
- }
2061
- if (this.mcpServers.length > 0) {
2062
- try {
2063
- await this.initializeMCPSchemas();
2064
- allToolDeclarations.push(
2065
- ...this.mcpToolSchemas.map((t) => ({
2066
- name: t.name,
2067
- description: t.description,
2068
- parameters: t.parameters
2069
- }))
2070
- );
2071
- } catch (error) {
2072
- console.warn("MCP initialization failed, skipping MCP tools:", error);
2127
+ const tokenLimit = this.resolveTokenLimit(model, maxTokens);
2128
+ if (isResponsesAPI) {
2129
+ if (tokenLimit !== void 0) {
2130
+ body.max_output_tokens = tokenLimit;
2073
2131
  }
2074
- }
2075
- if (allToolDeclarations.length > 0) {
2076
- body.tools = [
2077
- {
2078
- functionDeclarations: allToolDeclarations
2132
+ } else {
2133
+ if (tokenLimit !== void 0) {
2134
+ if (this.usesCompatibleChatCompletions()) {
2135
+ body.max_tokens = tokenLimit;
2136
+ } else {
2137
+ body.max_completion_tokens = tokenLimit;
2079
2138
  }
2080
- ];
2081
- body.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
2139
+ }
2082
2140
  }
2083
- const fetchOnce = async (ver, payload) => {
2084
- const fn = stream ? "streamGenerateContent" : "generateContent";
2085
- const alt = stream ? "?alt=sse" : "";
2086
- const url = `${ENDPOINT_GEMINI_API}/${ver}/models/${model}:${fn}${alt}${alt ? "&" : "?"}key=${this.apiKey}`;
2087
- return ChatServiceHttpClient.post(url, payload);
2088
- };
2089
- const isLite = /flash[-_]lite/.test(model);
2090
- const isGemma4 = this.isGemma4Model(model);
2091
- const isGemini25 = /gemini-2\.5/.test(model);
2092
- const isGemini3Preview = /^gemini-3(?:\.[0-9]+)?-.*preview/.test(model);
2093
- const requiresV1beta = isLite || isGemma4 || isGemini25 || isGemini3Preview;
2094
- const firstVer = requiresV1beta ? "v1beta" : "v1";
2095
- const tryApi = async () => {
2096
- try {
2097
- const payload = firstVer === "v1" ? body : this.adaptKeysForApi(body);
2098
- return await fetchOnce(firstVer, payload);
2099
- } catch (e) {
2100
- const looksLikeVersionMismatch = /Unknown name|Cannot find field|404/.test(e?.message || "") || e?.status === 404;
2101
- if (!requiresV1beta && looksLikeVersionMismatch) {
2102
- return await fetchOnce("v1beta", this.adaptKeysForApi(body));
2141
+ if (isResponsesAPI) {
2142
+ body.input = this.cleanMessagesForResponsesAPI(messages);
2143
+ } else {
2144
+ body.messages = this.provider === "mistral" ? this.cleanMessagesForMistralChatCompletions(messages) : messages;
2145
+ }
2146
+ if (isGPT5Model(model)) {
2147
+ if (isResponsesAPI) {
2148
+ if (this.reasoning_effort) {
2149
+ body.reasoning = {
2150
+ ...body.reasoning,
2151
+ effort: this.reasoning_effort
2152
+ };
2153
+ if (this.enableReasoningSummary) {
2154
+ body.reasoning.summary = "auto";
2155
+ }
2156
+ }
2157
+ if (this.verbosity) {
2158
+ body.text = {
2159
+ ...body.text,
2160
+ format: { type: "text" },
2161
+ verbosity: this.verbosity
2162
+ };
2163
+ }
2164
+ } else {
2165
+ if (this.reasoning_effort) {
2166
+ body.reasoning_effort = this.reasoning_effort;
2167
+ }
2168
+ if (this.verbosity) {
2169
+ body.verbosity = this.verbosity;
2103
2170
  }
2104
- throw e;
2105
2171
  }
2106
- };
2107
- try {
2108
- const res = await tryApi();
2109
- return res;
2110
- } catch (error) {
2111
- if (error.body) {
2112
- console.error("Gemini API Error Details:", error.body);
2113
- console.error("Request Body:", JSON.stringify(body, null, 2));
2172
+ }
2173
+ if (this.provider === "mistral" && isMistralReasoningEffortModel(model) && this.reasoning_effort && isMistralReasoningEffort(this.reasoning_effort)) {
2174
+ body.reasoning_effort = this.reasoning_effort;
2175
+ }
2176
+ const tools = this.buildToolsDefinition();
2177
+ if (tools.length > 0) {
2178
+ body.tools = tools;
2179
+ if (!isResponsesAPI) {
2180
+ body.tool_choice = "auto";
2114
2181
  }
2115
- throw error;
2116
2182
  }
2183
+ return body;
2117
2184
  }
2118
- /**
2119
- * Convert AITuber OnAir vision messages to Gemini format
2120
- * @param messages Array of vision messages
2121
- * @returns Gemini formatted vision messages
2185
+ resolveTokenLimit(model, maxTokens) {
2186
+ if (maxTokens !== void 0) {
2187
+ return maxTokens;
2188
+ }
2189
+ const baseTokenLimit = this.usesCompatibleChatCompletions() ? this.responseLength !== void 0 ? getMaxTokensForResponseLength(this.responseLength) : void 0 : getMaxTokensForResponseLength(this.responseLength);
2190
+ if (this.provider !== "openai" || !isGPT5Model(model) || this.responseLength === void 0) {
2191
+ return baseTokenLimit;
2192
+ }
2193
+ const effectiveReasoningEffort = this.reasoning_effort ?? getDefaultReasoningEffortForGPT5Model(model);
2194
+ return Math.max(
2195
+ baseTokenLimit ?? 0,
2196
+ GPT5_RESPONSE_LENGTH_MIN_TOKENS[this.responseLength],
2197
+ GPT5_REASONING_MIN_TOKENS[effectiveReasoningEffort]
2198
+ );
2199
+ }
2200
+ /**
2201
+ * Validate MCP servers compatibility with the current endpoint
2122
2202
  */
2123
- async convertVisionMessagesToGeminiFormat(messages) {
2124
- const geminiMessages = [];
2125
- let currentRole = null;
2126
- let currentParts = [];
2127
- for (const msg of messages) {
2128
- const role = this.mapRoleToGemini(msg.role);
2129
- if (msg.tool_calls) {
2130
- for (const call of msg.tool_calls) {
2131
- geminiMessages.push({
2132
- role: "model",
2133
- parts: [
2134
- {
2135
- functionCall: {
2136
- name: call.function.name,
2137
- args: JSON.parse(call.function.arguments || "{}")
2138
- }
2139
- }
2140
- ]
2141
- });
2142
- }
2143
- continue;
2144
- }
2145
- if (msg.role === "tool") {
2146
- const funcName = msg.name ?? this.callIdMap.get(msg.tool_call_id) ?? "result";
2147
- geminiMessages.push({
2148
- role: "user",
2149
- parts: [
2150
- {
2151
- functionResponse: {
2152
- name: funcName,
2153
- response: this.normalizeToolResult(
2154
- this.safeJsonParse(msg.content)
2155
- )
2156
- }
2157
- }
2158
- ]
2159
- });
2160
- continue;
2161
- }
2162
- if (role !== currentRole && currentParts.length > 0) {
2163
- geminiMessages.push({
2164
- role: currentRole,
2165
- parts: [...currentParts]
2166
- });
2167
- currentParts = [];
2168
- }
2169
- currentRole = role;
2203
+ validateMCPCompatibility() {
2204
+ if (this.mcpServers.length > 0 && this.endpoint === ENDPOINT_OPENAI_CHAT_COMPLETIONS_API) {
2205
+ throw new Error(
2206
+ `MCP servers are not supported with Chat Completions API. Current endpoint: ${this.endpoint}. Please use OpenAI Responses API endpoint: ${ENDPOINT_OPENAI_RESPONSES_API}. MCP tools are only available in the Responses API endpoint.`
2207
+ );
2208
+ }
2209
+ }
2210
+ /**
2211
+ * Clean messages for Responses API (remove timestamp and other extra properties)
2212
+ */
2213
+ cleanMessagesForResponsesAPI(messages) {
2214
+ return messages.map((msg) => {
2215
+ const role = msg.role === "tool" ? "user" : msg.role;
2216
+ const cleanMsg = {
2217
+ role
2218
+ };
2170
2219
  if (typeof msg.content === "string") {
2171
- currentParts.push({ text: msg.content });
2220
+ cleanMsg.content = msg.content;
2172
2221
  } else if (Array.isArray(msg.content)) {
2173
- for (const block of msg.content) {
2222
+ cleanMsg.content = msg.content.map((block) => {
2174
2223
  if (block.type === "text") {
2175
- currentParts.push({ text: block.text });
2224
+ return {
2225
+ type: "input_text",
2226
+ text: block.text
2227
+ };
2176
2228
  } else if (block.type === "image_url") {
2177
- try {
2178
- const imageResponse = await ChatServiceHttpClient.get(
2179
- block.image_url.url
2180
- );
2181
- const imageBlob = await imageResponse.blob();
2182
- const base64Data = await this.blobToBase64(imageBlob);
2183
- currentParts.push({
2184
- inlineData: {
2185
- mimeType: imageBlob.type || "image/jpeg",
2186
- data: base64Data.split(",")[1]
2187
- // Remove the "data:image/jpeg;base64," prefix
2188
- }
2189
- });
2190
- } catch (error) {
2191
- console.error("Error processing image:", error);
2192
- throw new Error(`Failed to process image: ${error.message}`);
2193
- }
2229
+ return {
2230
+ type: "input_image",
2231
+ image_url: block.image_url.url
2232
+ // Extract the URL string directly
2233
+ };
2194
2234
  }
2195
- }
2235
+ return block;
2236
+ });
2237
+ } else {
2238
+ cleanMsg.content = msg.content;
2196
2239
  }
2197
- }
2198
- if (currentRole && currentParts.length > 0) {
2199
- geminiMessages.push({
2200
- role: currentRole,
2201
- parts: [...currentParts]
2202
- });
2203
- }
2204
- return geminiMessages;
2240
+ return cleanMsg;
2241
+ });
2205
2242
  }
2206
- /**
2207
- * Convert Blob to Base64 string
2208
- * @param blob Image blob
2209
- * @returns Promise with base64 encoded string
2210
- */
2211
- blobToBase64(blob) {
2212
- return new Promise((resolve, reject) => {
2213
- const reader = new FileReader();
2214
- reader.onloadend = () => resolve(reader.result);
2215
- reader.onerror = reject;
2216
- reader.readAsDataURL(blob);
2243
+ cleanMessagesForMistralChatCompletions(messages) {
2244
+ return messages.map((msg) => {
2245
+ if (!Array.isArray(msg.content)) {
2246
+ return msg;
2247
+ }
2248
+ return {
2249
+ ...msg,
2250
+ content: msg.content.map((block) => {
2251
+ if (block.type === "image_url" && typeof block.image_url === "object" && typeof block.image_url?.url === "string") {
2252
+ return {
2253
+ type: "image_url",
2254
+ image_url: block.image_url.url
2255
+ };
2256
+ }
2257
+ return block;
2258
+ })
2259
+ };
2217
2260
  });
2218
2261
  }
2219
2262
  /**
2220
- * Map AITuber OnAir roles to Gemini roles
2221
- * @param role AITuber OnAir role
2222
- * @returns Gemini role
2263
+ * Build tools definition based on the endpoint type
2223
2264
  */
2224
- mapRoleToGemini(role) {
2225
- switch (role) {
2226
- case "system":
2227
- return "model";
2228
- // Gemini uses 'model' for system messages
2229
- case "user":
2230
- return "user";
2231
- case "assistant":
2232
- return "model";
2233
- default:
2234
- return "user";
2265
+ buildToolsDefinition() {
2266
+ const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2267
+ const toolDefs = [];
2268
+ if (this.tools.length > 0) {
2269
+ toolDefs.push(
2270
+ ...buildOpenAICompatibleTools(
2271
+ this.tools,
2272
+ isResponsesAPI ? "responses" : "chat-completions"
2273
+ )
2274
+ );
2275
+ }
2276
+ if (this.mcpServers.length > 0 && isResponsesAPI) {
2277
+ toolDefs.push(...this.buildMCPToolsDefinition());
2235
2278
  }
2279
+ return toolDefs;
2236
2280
  }
2237
- /* ────────────────────────────────────────────────────────── */
2238
- /* Convert NDJSON stream to common format */
2239
- /* ────────────────────────────────────────────────────────── */
2240
- async parseStream(res, onPartial, model) {
2241
- const reader = res.body.getReader();
2242
- const dec = new TextDecoder();
2243
- const textBlocks = [];
2244
- const toolBlocks = [];
2245
- let buf = "";
2246
- const flush = (payload) => {
2247
- if (!payload || payload === "[DONE]") return;
2248
- let obj;
2249
- try {
2250
- obj = JSON.parse(payload);
2251
- } catch {
2252
- return;
2281
+ /**
2282
+ * Build MCP tools definition for Responses API
2283
+ */
2284
+ buildMCPToolsDefinition() {
2285
+ return this.mcpServers.map((server) => {
2286
+ const mcpDef = {
2287
+ type: "mcp",
2288
+ // Using 'mcp' as indicated by the error message
2289
+ server_label: server.name,
2290
+ // Use server_label as required by API
2291
+ server_url: server.url
2292
+ // Use server_url instead of url
2293
+ };
2294
+ if (server.require_approval) {
2295
+ mcpDef.require_approval = server.require_approval;
2253
2296
  }
2254
- for (const cand of obj.candidates ?? []) {
2255
- for (const part of cand.content?.parts ?? []) {
2256
- if (this.shouldExposeTextPart(part, model)) {
2257
- onPartial(part.text);
2258
- StreamTextAccumulator.addTextBlock(textBlocks, part.text);
2259
- }
2260
- if (part.functionCall) {
2261
- toolBlocks.push({
2262
- type: "tool_use",
2263
- id: this.genUUID(),
2264
- name: part.functionCall.name,
2265
- input: part.functionCall.args ?? {}
2266
- });
2267
- }
2268
- if (part.functionResponse) {
2269
- toolBlocks.push({
2270
- type: "tool_result",
2271
- tool_use_id: part.functionResponse.name,
2272
- content: JSON.stringify(part.functionResponse.response)
2273
- });
2274
- }
2275
- }
2297
+ if (server.tool_configuration?.allowed_tools) {
2298
+ mcpDef.allowed_tools = server.tool_configuration.allowed_tools;
2276
2299
  }
2277
- };
2278
- while (true) {
2279
- const { done, value } = await reader.read();
2280
- if (done) break;
2281
- buf += dec.decode(value, { stream: true });
2282
- let nl;
2283
- while ((nl = buf.indexOf("\n")) !== -1) {
2284
- let line = buf.slice(0, nl);
2285
- buf = buf.slice(nl + 1);
2286
- if (line.endsWith("\r")) line = line.slice(0, -1);
2287
- if (!line.trim()) {
2288
- flush("");
2289
- continue;
2290
- }
2291
- if (line.startsWith("data:")) line = line.slice(5).trim();
2292
- if (!line) continue;
2293
- flush(line);
2300
+ if (server.authorization_token) {
2301
+ mcpDef.headers = {
2302
+ Authorization: `Bearer ${server.authorization_token}`
2303
+ };
2294
2304
  }
2295
- }
2296
- if (buf) flush(buf);
2297
- const blocks = [...textBlocks, ...toolBlocks];
2298
- return {
2299
- blocks,
2300
- stop_reason: toolBlocks.some((b) => b.type === "tool_use") ? "tool_use" : "end"
2301
- };
2305
+ return mcpDef;
2306
+ });
2302
2307
  }
2303
- /* ────────────────────────────────────────────────────────── */
2304
- /* Convert JSON of non-stream (= generateContent) */
2305
- /* ────────────────────────────────────────────────────────── */
2306
- parseOneShot(data, model) {
2307
- const textBlocks = [];
2308
- const toolBlocks = [];
2309
- for (const cand of data.candidates ?? []) {
2310
- for (const part of cand.content?.parts ?? []) {
2311
- if (this.shouldExposeTextPart(part, model)) {
2312
- textBlocks.push({ type: "text", text: part.text });
2313
- }
2314
- if (part.functionCall) {
2315
- toolBlocks.push({
2316
- type: "tool_use",
2317
- id: this.genUUID(),
2318
- name: part.functionCall.name,
2319
- input: part.functionCall.args ?? {}
2320
- });
2321
- }
2322
- if (part.functionResponse) {
2323
- toolBlocks.push({
2324
- type: "tool_result",
2325
- tool_use_id: part.functionResponse.name,
2326
- content: JSON.stringify(part.functionResponse.response)
2327
- });
2328
- }
2329
- }
2330
- }
2331
- const blocks = [...textBlocks, ...toolBlocks];
2332
- return {
2333
- blocks,
2334
- stop_reason: toolBlocks.some((b) => b.type === "tool_use") ? "tool_use" : "end"
2335
- };
2308
+ async handleStream(res, onPartial) {
2309
+ return parseOpenAICompatibleTextStream(res, onPartial);
2336
2310
  }
2337
- /* ────────────────────────────────────────────────────────── */
2338
- /* chatOnce (text) */
2339
- /* ────────────────────────────────────────────────────────── */
2340
- async chatOnce(messages, stream = true, onPartialResponse = () => {
2341
- }, maxTokens) {
2342
- const res = await this.callGemini(messages, this.model, stream, maxTokens);
2343
- return stream ? this.parseStream(res, onPartialResponse, this.model) : this.parseOneShot(await res.json(), this.model);
2311
+ async parseStream(res, onPartial) {
2312
+ return parseOpenAICompatibleToolStream(res, onPartial, {
2313
+ appendTextBlock: StreamTextAccumulator.addTextBlock
2314
+ });
2344
2315
  }
2345
- /* ────────────────────────────────────────────────────────── */
2346
- /* visionChatOnce (images) */
2347
- /* ────────────────────────────────────────────────────────── */
2348
- async visionChatOnce(messages, stream = false, onPartialResponse = () => {
2349
- }, maxTokens) {
2350
- const res = await this.callGemini(
2351
- messages,
2352
- this.visionModel,
2353
- stream,
2354
- maxTokens
2355
- );
2356
- return stream ? this.parseStream(res, onPartialResponse, this.visionModel) : this.parseOneShot(await res.json(), this.visionModel);
2316
+ parseOneShot(data) {
2317
+ return parseOpenAICompatibleOneShot(data);
2357
2318
  }
2358
- /* ────────────────────────────────────────────────────────── */
2359
- /* UUID helper */
2360
- /* ────────────────────────────────────────────────────────── */
2361
- genUUID() {
2362
- return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
2363
- const r = Math.random() * 16 | 0;
2364
- const v = c === "x" ? r : r & 3 | 8;
2365
- return v.toString(16);
2366
- });
2319
+ usesCompatibleChatCompletions() {
2320
+ return OPENAI_COMPATIBLE_CHAT_COMPLETIONS_PROVIDERS.has(this.provider);
2367
2321
  }
2368
2322
  };
2369
2323
 
2370
- // src/services/providers/gemini/GeminiChatServiceProvider.ts
2371
- var GeminiChatServiceProvider = class {
2372
- /**
2373
- * Create a chat service instance
2374
- * @param options Service options
2375
- * @returns GeminiChatService instance
2376
- */
2324
+ // src/services/providers/deepseek/DeepSeekChatService.ts
2325
+ var DeepSeekChatService = class extends OpenAIChatService {
2326
+ constructor(apiKey, model = MODEL_DEEPSEEK_V4_FLASH, visionModel = model, tools, endpoint = ENDPOINT_DEEPSEEK_CHAT_COMPLETIONS_API, responseLength) {
2327
+ super(
2328
+ apiKey,
2329
+ model,
2330
+ visionModel,
2331
+ tools,
2332
+ endpoint,
2333
+ [],
2334
+ responseLength,
2335
+ void 0,
2336
+ void 0,
2337
+ false,
2338
+ "deepseek",
2339
+ false
2340
+ );
2341
+ }
2342
+ };
2343
+
2344
+ // src/services/providers/deepseek/DeepSeekChatServiceProvider.ts
2345
+ var DeepSeekChatServiceProvider = class {
2377
2346
  createChatService(options) {
2378
- const visionModel = resolveVisionModel({
2379
- model: options.model,
2380
- visionModel: options.visionModel,
2381
- defaultModel: this.getDefaultModel(),
2382
- defaultVisionModel: this.getDefaultModel(),
2383
- supportsVisionForModel: (model) => this.supportsVisionForModel(model),
2384
- validate: "resolved"
2385
- });
2386
- return new GeminiChatService(
2347
+ this.validateRequiredOptions(options);
2348
+ const model = options.model || this.getDefaultModel();
2349
+ const tools = options.tools;
2350
+ return new DeepSeekChatService(
2387
2351
  options.apiKey,
2388
- options.model || this.getDefaultModel(),
2389
- visionModel,
2390
- options.tools || [],
2391
- options.mcpServers || [],
2352
+ model,
2353
+ options.visionModel ?? model,
2354
+ tools,
2355
+ this.resolveEndpoint(options),
2392
2356
  options.responseLength
2393
2357
  );
2394
2358
  }
2395
- /**
2396
- * Get the provider name
2397
- * @returns Provider name ('gemini')
2398
- */
2399
2359
  getProviderName() {
2400
- return "gemini";
2360
+ return "deepseek";
2401
2361
  }
2402
- /**
2403
- * Get the list of supported models
2404
- * @returns Array of supported model names
2405
- */
2406
2362
  getSupportedModels() {
2407
- return [
2408
- MODEL_GEMMA_4_31B_IT,
2409
- MODEL_GEMMA_4_26B_A4B_IT,
2410
- MODEL_GEMINI_3_1_PRO_PREVIEW,
2411
- MODEL_GEMINI_3_1_FLASH_LITE_PREVIEW,
2412
- MODEL_GEMINI_3_PRO_PREVIEW,
2413
- MODEL_GEMINI_3_FLASH_PREVIEW,
2414
- MODEL_GEMINI_2_5_PRO,
2415
- MODEL_GEMINI_2_5_FLASH,
2416
- MODEL_GEMINI_2_5_FLASH_LITE,
2417
- MODEL_GEMINI_2_5_FLASH_LITE_PREVIEW_06_17,
2418
- MODEL_GEMINI_2_0_FLASH,
2419
- MODEL_GEMINI_2_0_FLASH_LITE
2420
- ];
2363
+ return [...DEEPSEEK_SUPPORTED_MODELS];
2421
2364
  }
2422
- /**
2423
- * Get the default model
2424
- * @returns Default model name
2425
- */
2426
2365
  getDefaultModel() {
2427
- return MODEL_GEMINI_2_0_FLASH_LITE;
2366
+ return MODEL_DEEPSEEK_V4_FLASH;
2428
2367
  }
2429
- /**
2430
- * Check if this provider supports vision (image processing)
2431
- * @returns Vision support status (true)
2432
- */
2433
2368
  supportsVision() {
2434
- return this.getVisionSupportLevel() !== "unsupported";
2369
+ return false;
2435
2370
  }
2436
2371
  getVisionSupportLevel() {
2437
- return "supported";
2438
- }
2439
- /**
2440
- * Check if a specific model supports vision capabilities
2441
- * @param model The model name to check
2442
- * @returns True if the model supports vision, false otherwise
2443
- */
2444
- supportsVisionForModel(model) {
2445
- return GEMINI_VISION_SUPPORTED_MODELS.includes(model);
2446
- }
2447
- getVisionSupportLevelForModel(model) {
2448
- return this.supportsVisionForModel(model) ? "supported" : "unsupported";
2449
- }
2450
- };
2451
-
2452
- // src/services/providers/geminiNano/GeminiNanoChatService.ts
2453
- function getLanguageModelAPI() {
2454
- if (typeof globalThis !== "undefined" && "LanguageModel" in globalThis) {
2455
- return globalThis.LanguageModel;
2456
- }
2457
- return void 0;
2458
- }
2459
- var GeminiNanoChatService = class {
2460
- constructor(options = {}) {
2461
- this.provider = "gemini-nano";
2462
- this.expectedInputLanguages = options.expectedInputLanguages ?? ["ja"];
2463
- this.expectedOutputLanguages = options.expectedOutputLanguages ?? ["ja"];
2464
- this._responseLength = options.responseLength;
2465
- }
2466
- getModel() {
2467
- return MODEL_GEMINI_NANO;
2468
- }
2469
- getVisionModel() {
2470
- return MODEL_GEMINI_NANO;
2471
- }
2472
- /**
2473
- * Process chat messages using Gemini Nano.
2474
- * Non-streaming: calls onPartialResponse once with the full response,
2475
- * then calls onCompleteResponse.
2476
- */
2477
- async processChat(messages, onPartialResponse, onCompleteResponse) {
2478
- const response = await this.generateResponse(messages);
2479
- onPartialResponse(response);
2480
- await onCompleteResponse(response);
2481
- }
2482
- async processVisionChat(_messages, _onPartialResponse, _onCompleteResponse) {
2483
- throw new Error("Gemini Nano does not support vision capabilities.");
2372
+ return "unsupported";
2484
2373
  }
2485
- async chatOnce(messages, _stream = false, onPartialResponse = () => {
2486
- }, _maxTokens) {
2487
- const response = await this.generateResponse(messages);
2488
- onPartialResponse(response);
2489
- return {
2490
- blocks: [{ type: "text", text: response }],
2491
- stop_reason: "end"
2492
- };
2374
+ supportsVisionForModel(_model) {
2375
+ return false;
2493
2376
  }
2494
- async visionChatOnce(_messages, _stream = false, _onPartialResponse = () => {
2495
- }, _maxTokens) {
2496
- throw new Error("Gemini Nano does not support vision capabilities.");
2377
+ getVisionSupportLevelForModel(_model) {
2378
+ return "unsupported";
2497
2379
  }
2498
- /**
2499
- * Core logic: extract system prompt, manage session, call prompt().
2500
- */
2501
- async generateResponse(messages) {
2502
- const api = getLanguageModelAPI();
2503
- if (!api) {
2504
- throw new Error(
2505
- "Gemini Nano is not available in this environment. Chrome 138+ with Prompt API enabled is required."
2506
- );
2507
- }
2508
- const availability = await api.availability();
2509
- if (availability !== "available" && availability !== "downloadable") {
2510
- throw new Error(
2511
- `Gemini Nano Prompt API is not ready in this environment. LanguageModel.availability() returned "${availability}". Expected "available" or "downloadable".`
2512
- );
2380
+ validateRequiredOptions(options) {
2381
+ if (!options.apiKey?.trim()) {
2382
+ throw new Error("deepseek provider requires apiKey.");
2513
2383
  }
2514
- const systemMessages = messages.filter((m) => m.role === "system");
2515
- const systemPrompt = systemMessages.map((m) => m.content).join("\n");
2516
- const conversationMessages = messages.filter((m) => m.role !== "system").slice(-GEMINI_NANO_MAX_CONTEXT_MESSAGES);
2517
- const lastUserMessage = [...conversationMessages].reverse().find((m) => m.role === "user");
2518
- if (!lastUserMessage) {
2519
- throw new Error("No user message found in the provided messages.");
2384
+ }
2385
+ resolveEndpoint(options) {
2386
+ if (options.endpoint) {
2387
+ return this.normalizeUrl(options.endpoint);
2520
2388
  }
2521
- const session = await this.createSession(
2522
- api,
2523
- systemPrompt,
2524
- conversationMessages
2525
- );
2526
- try {
2527
- return await session.prompt(lastUserMessage.content);
2528
- } finally {
2529
- try {
2530
- session.destroy();
2531
- } catch {
2389
+ if (options.baseUrl) {
2390
+ const baseUrl = this.normalizeUrl(options.baseUrl);
2391
+ if (baseUrl.endsWith("/chat/completions")) {
2392
+ return baseUrl;
2532
2393
  }
2394
+ return `${baseUrl}/chat/completions`;
2533
2395
  }
2396
+ return ENDPOINT_DEEPSEEK_CHAT_COMPLETIONS_API;
2534
2397
  }
2398
+ normalizeUrl(value) {
2399
+ return value.trim().replace(/\/+$/, "");
2400
+ }
2401
+ };
2402
+
2403
+ // src/utils/mcpSchemaFetcher.ts
2404
+ var MCPSchemaFetcher = class {
2535
2405
  /**
2536
- * Create a new LanguageModel session with system prompt and context history.
2537
- * Context history (excluding the last user message) is embedded in the system prompt.
2406
+ * Fetch tool schemas from MCP server
2407
+ * @param serverConfig MCP server configuration
2408
+ * @returns Array of tool definitions
2538
2409
  */
2539
- async createSession(api, systemPrompt, contextHistory) {
2540
- let prompt = this.buildSystemPrompt(systemPrompt);
2541
- const historyMessages = contextHistory.slice(0, -1);
2542
- if (historyMessages.length > 0) {
2543
- const history = historyMessages.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n");
2544
- prompt += "\n\nThe following is the prior conversation history. Use it as context for your response:\n" + history;
2545
- }
2546
- return api.create({
2547
- systemPrompt: prompt,
2548
- expectedInputs: [
2549
- { type: "text", languages: this.expectedInputLanguages }
2550
- ],
2551
- expectedOutputs: [
2552
- { type: "text", languages: this.expectedOutputLanguages }
2553
- ]
2554
- });
2555
- }
2556
- buildSystemPrompt(systemPrompt) {
2557
- const promptParts = [];
2558
- if (systemPrompt) {
2559
- promptParts.push(systemPrompt);
2560
- }
2561
- const lengthInstruction = this.getResponseLengthInstruction();
2562
- if (lengthInstruction) {
2563
- promptParts.push(lengthInstruction);
2410
+ static async fetchToolSchemas(serverConfig) {
2411
+ try {
2412
+ const headers = {
2413
+ "Content-Type": "application/json"
2414
+ };
2415
+ if (serverConfig.authorization_token) {
2416
+ headers["Authorization"] = `Bearer ${serverConfig.authorization_token}`;
2417
+ }
2418
+ const response = await ChatServiceHttpClient.post(
2419
+ `${serverConfig.url}/tools`,
2420
+ {},
2421
+ headers
2422
+ );
2423
+ const toolsData = await response.json();
2424
+ if (Array.isArray(toolsData.tools)) {
2425
+ return toolsData.tools.map((tool) => ({
2426
+ name: `mcp_${serverConfig.name}_${tool.name}`,
2427
+ description: tool.description || `Tool from ${serverConfig.name} MCP server`,
2428
+ parameters: tool.inputSchema || {
2429
+ type: "object",
2430
+ properties: {},
2431
+ required: []
2432
+ }
2433
+ }));
2434
+ }
2435
+ return [
2436
+ {
2437
+ name: `mcp_${serverConfig.name}_search`,
2438
+ description: `Search using ${serverConfig.name} MCP server`,
2439
+ parameters: {
2440
+ type: "object",
2441
+ properties: {
2442
+ query: {
2443
+ type: "string",
2444
+ description: "Search query"
2445
+ }
2446
+ },
2447
+ required: ["query"]
2448
+ }
2449
+ }
2450
+ ];
2451
+ } catch (error) {
2452
+ console.warn(
2453
+ `Failed to fetch MCP schemas from ${serverConfig.name}:`,
2454
+ error
2455
+ );
2456
+ return [
2457
+ {
2458
+ name: `mcp_${serverConfig.name}_search`,
2459
+ description: `Search using ${serverConfig.name} MCP server (schema fetch failed)`,
2460
+ parameters: {
2461
+ type: "object",
2462
+ properties: {
2463
+ query: {
2464
+ type: "string",
2465
+ description: "Search query"
2466
+ }
2467
+ },
2468
+ required: ["query"]
2469
+ }
2470
+ }
2471
+ ];
2564
2472
  }
2565
- return promptParts.join("\n\n");
2566
2473
  }
2567
- getResponseLengthInstruction() {
2568
- if (!this._responseLength) {
2569
- return void 0;
2570
- }
2571
- const maxTokens = MAX_TOKENS_BY_LENGTH[this._responseLength];
2572
- if (!maxTokens) {
2573
- return void 0;
2474
+ /**
2475
+ * Fetch all tool schemas from multiple MCP servers
2476
+ * @param mcpServers Array of MCP server configurations
2477
+ * @returns Array of all tool definitions
2478
+ */
2479
+ static async fetchAllToolSchemas(mcpServers) {
2480
+ const allSchemas = [];
2481
+ for (const server of mcpServers) {
2482
+ try {
2483
+ const schemas = await this.fetchToolSchemas(server);
2484
+ allSchemas.push(...schemas);
2485
+ } catch (error) {
2486
+ console.error(`Failed to fetch schemas from ${server.name}:`, error);
2487
+ }
2574
2488
  }
2575
- return `Please keep your response concise, within approximately ${maxTokens} tokens.`;
2576
- }
2577
- };
2578
-
2579
- // src/services/providers/geminiNano/GeminiNanoChatServiceProvider.ts
2580
- var GeminiNanoChatServiceProvider = class {
2581
- createChatService(options) {
2582
- return new GeminiNanoChatService({
2583
- expectedInputLanguages: options.expectedInputLanguages,
2584
- expectedOutputLanguages: options.expectedOutputLanguages,
2585
- responseLength: options.responseLength
2586
- });
2587
- }
2588
- getProviderName() {
2589
- return "gemini-nano";
2590
- }
2591
- getSupportedModels() {
2592
- return [MODEL_GEMINI_NANO];
2593
- }
2594
- getDefaultModel() {
2595
- return MODEL_GEMINI_NANO;
2596
- }
2597
- supportsVision() {
2598
- return false;
2599
- }
2600
- getVisionSupportLevel() {
2601
- return "unsupported";
2489
+ return allSchemas;
2602
2490
  }
2603
2491
  };
2604
2492
 
2605
- // src/services/providers/kimi/KimiChatService.ts
2606
- var KimiChatService = class {
2493
+ // src/services/providers/gemini/GeminiChatService.ts
2494
+ var GeminiChatService = class {
2607
2495
  /**
2608
2496
  * Constructor
2609
- * @param apiKey Kimi API key
2497
+ * @param apiKey Google API key
2610
2498
  * @param model Name of the model to use
2611
2499
  * @param visionModel Name of the vision model
2500
+ * @param tools Array of tool definitions
2501
+ * @param mcpServers Array of MCP server configurations
2612
2502
  */
2613
- constructor(apiKey, model = MODEL_KIMI_K2_6, visionModel = MODEL_KIMI_K2_6, tools, endpoint = ENDPOINT_KIMI_CHAT_COMPLETIONS_API, responseLength, responseFormat, thinking) {
2503
+ constructor(apiKey, model = MODEL_GEMINI_3_1_FLASH_LITE, visionModel = MODEL_GEMINI_3_1_FLASH_LITE, tools = [], mcpServers = [], responseLength) {
2614
2504
  /** Provider name */
2615
- this.provider = "kimi";
2505
+ this.provider = "gemini";
2506
+ this.mcpToolSchemas = [];
2507
+ this.mcpSchemasInitialized = false;
2508
+ /** id(OpenAI) → name(Gemini) mapping */
2509
+ this.callIdMap = /* @__PURE__ */ new Map();
2616
2510
  this.apiKey = apiKey;
2617
2511
  this.model = model;
2618
- this.tools = tools || [];
2619
- this.endpoint = endpoint;
2620
2512
  this.responseLength = responseLength;
2621
- this.responseFormat = responseFormat;
2622
- this.thinking = thinking ?? { type: "enabled" };
2513
+ if (!GEMINI_VISION_SUPPORTED_MODELS.includes(visionModel)) {
2514
+ throw new Error(
2515
+ `Model ${visionModel} does not support vision capabilities.`
2516
+ );
2517
+ }
2623
2518
  this.visionModel = visionModel;
2519
+ this.tools = tools;
2520
+ this.mcpServers = mcpServers;
2521
+ }
2522
+ /* ────────────────────────────────── */
2523
+ /* Utilities */
2524
+ /* ────────────────────────────────── */
2525
+ safeJsonParse(str) {
2526
+ try {
2527
+ return JSON.parse(str);
2528
+ } catch {
2529
+ return str;
2530
+ }
2531
+ }
2532
+ normalizeToolResult(val) {
2533
+ if (val === null) return { content: null };
2534
+ if (typeof val === "object") return val;
2535
+ return { content: val };
2536
+ }
2537
+ isGemma4Model(model) {
2538
+ return /^gemma-4-/.test(model);
2539
+ }
2540
+ shouldExposeTextPart(part, model) {
2541
+ if (!part.text) return false;
2542
+ if (this.isGemma4Model(model) && part.thought === true) {
2543
+ return false;
2544
+ }
2545
+ return true;
2546
+ }
2547
+ /**
2548
+ * camelCase → snake_case conversion (v1beta)
2549
+ */
2550
+ adaptKeysForApi(obj) {
2551
+ const map = {
2552
+ toolConfig: "tool_config",
2553
+ functionCallingConfig: "function_calling_config",
2554
+ functionDeclarations: "function_declarations",
2555
+ functionCall: "function_call",
2556
+ functionResponse: "function_response"
2557
+ };
2558
+ if (Array.isArray(obj)) return obj.map((v) => this.adaptKeysForApi(v));
2559
+ if (obj && typeof obj === "object") {
2560
+ return Object.fromEntries(
2561
+ Object.entries(obj).map(([k, v]) => [
2562
+ map[k] ?? k,
2563
+ this.adaptKeysForApi(v)
2564
+ ])
2565
+ );
2566
+ }
2567
+ return obj;
2624
2568
  }
2625
2569
  /**
2626
2570
  * Get the current model name
2571
+ * @returns Model name
2627
2572
  */
2628
2573
  getModel() {
2629
2574
  return this.model;
2630
2575
  }
2631
2576
  /**
2632
2577
  * Get the current vision model name
2578
+ * @returns Vision model name
2633
2579
  */
2634
2580
  getVisionModel() {
2635
2581
  return this.visionModel;
2636
2582
  }
2637
2583
  /**
2638
- * Process chat messages
2584
+ * Get configured MCP servers
2585
+ * @returns Array of MCP server configurations
2639
2586
  */
2640
- async processChat(messages, onPartialResponse, onCompleteResponse) {
2641
- await processChatWithOptionalTools({
2642
- hasTools: this.tools.length > 0,
2643
- runWithoutTools: async () => {
2644
- const res = await this.callKimi(messages, this.model, true);
2645
- return this.handleStream(res, onPartialResponse);
2646
- },
2647
- runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
2648
- onCompleteResponse,
2649
- toolErrorMessage: "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
2650
- });
2587
+ getMCPServers() {
2588
+ return this.mcpServers;
2651
2589
  }
2652
2590
  /**
2653
- * Process chat messages with images
2591
+ * Add MCP server configuration
2592
+ * @param serverConfig MCP server configuration
2654
2593
  */
2655
- async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
2656
- if (!isKimiVisionModel(this.visionModel)) {
2657
- throw new Error(
2658
- `Model ${this.visionModel} does not support vision capabilities.`
2659
- );
2594
+ addMCPServer(serverConfig) {
2595
+ this.mcpServers.push(serverConfig);
2596
+ this.mcpSchemasInitialized = false;
2597
+ }
2598
+ /**
2599
+ * Remove MCP server by name
2600
+ * @param serverName Name of the server to remove
2601
+ */
2602
+ removeMCPServer(serverName) {
2603
+ this.mcpServers = this.mcpServers.filter(
2604
+ (server) => server.name !== serverName
2605
+ );
2606
+ this.mcpSchemasInitialized = false;
2607
+ }
2608
+ /**
2609
+ * Check if MCP servers are configured
2610
+ * @returns True if MCP servers are configured
2611
+ */
2612
+ hasMCPServers() {
2613
+ return this.mcpServers.length > 0;
2614
+ }
2615
+ /**
2616
+ * Initialize MCP tool schemas by fetching from servers
2617
+ * @private
2618
+ */
2619
+ async initializeMCPSchemas() {
2620
+ if (this.mcpSchemasInitialized || this.mcpServers.length === 0) {
2621
+ return;
2622
+ }
2623
+ try {
2624
+ const timeoutPromise = new Promise(
2625
+ (_, reject) => setTimeout(() => reject(new Error("MCP schema fetch timeout")), 5e3)
2626
+ );
2627
+ const schemasPromise = MCPSchemaFetcher.fetchAllToolSchemas(
2628
+ this.mcpServers
2629
+ );
2630
+ this.mcpToolSchemas = await Promise.race([
2631
+ schemasPromise,
2632
+ timeoutPromise
2633
+ ]);
2634
+ this.mcpSchemasInitialized = true;
2635
+ } catch (error) {
2636
+ console.warn("Failed to initialize MCP schemas, using fallback:", error);
2637
+ this.mcpToolSchemas = this.mcpServers.map((server) => ({
2638
+ name: `mcp_${server.name}_search`,
2639
+ description: `Search using ${server.name} MCP server (fallback)`,
2640
+ parameters: {
2641
+ type: "object",
2642
+ properties: {
2643
+ query: {
2644
+ type: "string",
2645
+ description: "Search query"
2646
+ }
2647
+ },
2648
+ required: ["query"]
2649
+ }
2650
+ }));
2651
+ this.mcpSchemasInitialized = true;
2652
+ }
2653
+ }
2654
+ /**
2655
+ * Process chat messages
2656
+ * @param messages Array of messages to send
2657
+ * @param onPartialResponse Callback to receive each part of streaming response
2658
+ * @param onCompleteResponse Callback to execute when response is complete
2659
+ */
2660
+ async processChat(messages, onPartialResponse, onCompleteResponse) {
2661
+ try {
2662
+ await processChatWithOptionalTools({
2663
+ hasTools: this.tools.length > 0 || this.mcpServers.length > 0,
2664
+ runWithoutTools: async () => {
2665
+ const res = await this.callGemini(messages, this.model, true);
2666
+ const { blocks } = await this.parseStream(
2667
+ res,
2668
+ onPartialResponse,
2669
+ this.model
2670
+ );
2671
+ return StreamTextAccumulator.getFullText(blocks);
2672
+ },
2673
+ runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
2674
+ onCompleteResponse,
2675
+ toolErrorMessage: "Received functionCall. Use chatOnce() loop when tools are enabled."
2676
+ });
2677
+ } catch (err) {
2678
+ console.error("Error in processChat:", err);
2679
+ throw err;
2680
+ }
2681
+ }
2682
+ async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
2683
+ try {
2684
+ await processChatWithOptionalTools({
2685
+ hasTools: this.tools.length > 0 || this.mcpServers.length > 0,
2686
+ runWithoutTools: async () => {
2687
+ const res = await this.callGemini(messages, this.visionModel, true);
2688
+ const { blocks } = await this.parseStream(
2689
+ res,
2690
+ onPartialResponse,
2691
+ this.visionModel
2692
+ );
2693
+ return StreamTextAccumulator.getFullText(blocks);
2694
+ },
2695
+ runWithTools: () => this.visionChatOnce(messages),
2696
+ onToolBlocks: (blocks) => {
2697
+ blocks.filter(
2698
+ (b) => b.type === "text"
2699
+ ).forEach((b) => onPartialResponse(b.text));
2700
+ },
2701
+ onCompleteResponse,
2702
+ toolErrorMessage: "Received functionCall. Use visionChatOnce() loop when tools are enabled."
2703
+ });
2704
+ } catch (err) {
2705
+ console.error("Error in processVisionChat:", err);
2706
+ throw err;
2707
+ }
2708
+ }
2709
+ /* ────────────────────────────────── */
2710
+ /* OpenAI → Gemini conversion */
2711
+ /* ────────────────────────────────── */
2712
+ convertMessagesToGeminiFormat(messages) {
2713
+ const gemini = [];
2714
+ let currentRole = null;
2715
+ let currentParts = [];
2716
+ const pushCurrent = () => {
2717
+ if (currentRole && currentParts.length) {
2718
+ gemini.push({ role: currentRole, parts: [...currentParts] });
2719
+ currentParts = [];
2720
+ }
2721
+ };
2722
+ for (const msg of messages) {
2723
+ const role = this.mapRoleToGemini(msg.role);
2724
+ if (msg.tool_calls) {
2725
+ pushCurrent();
2726
+ for (const call of msg.tool_calls) {
2727
+ this.callIdMap.set(call.id, call.function.name);
2728
+ gemini.push({
2729
+ role: "model",
2730
+ parts: [
2731
+ {
2732
+ functionCall: {
2733
+ name: call.function.name,
2734
+ args: JSON.parse(call.function.arguments || "{}")
2735
+ }
2736
+ }
2737
+ ]
2738
+ });
2739
+ }
2740
+ continue;
2741
+ }
2742
+ if (msg.role === "tool") {
2743
+ pushCurrent();
2744
+ const funcName = msg.name ?? this.callIdMap.get(msg.tool_call_id) ?? "result";
2745
+ gemini.push({
2746
+ role: "user",
2747
+ parts: [
2748
+ {
2749
+ functionResponse: {
2750
+ name: funcName,
2751
+ response: this.normalizeToolResult(
2752
+ this.safeJsonParse(msg.content)
2753
+ )
2754
+ }
2755
+ }
2756
+ ]
2757
+ });
2758
+ continue;
2759
+ }
2760
+ if (role !== currentRole) pushCurrent();
2761
+ currentRole = role;
2762
+ currentParts.push({ text: msg.content });
2763
+ }
2764
+ pushCurrent();
2765
+ return gemini;
2766
+ }
2767
+ /* ────────────────────────────────── */
2768
+ /* HTTP call */
2769
+ /* ────────────────────────────────── */
2770
+ async callGemini(messages, model, stream = false, maxTokens) {
2771
+ const hasVision = messages.some(
2772
+ (m) => Array.isArray(m.content) && m.content.some(
2773
+ (b) => b?.type === "image_url" || b?.inlineData
2774
+ )
2775
+ );
2776
+ const contents = hasVision ? await this.convertVisionMessagesToGeminiFormat(
2777
+ messages
2778
+ ) : this.convertMessagesToGeminiFormat(messages);
2779
+ const body = {
2780
+ contents,
2781
+ generationConfig: {
2782
+ maxOutputTokens: maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength)
2783
+ }
2784
+ };
2785
+ if (this.isGemma4Model(model)) {
2786
+ body.generationConfig.thinkingConfig = {
2787
+ includeThoughts: false,
2788
+ thinkingLevel: "minimal"
2789
+ };
2790
+ }
2791
+ const allToolDeclarations = [];
2792
+ if (this.tools.length > 0) {
2793
+ allToolDeclarations.push(
2794
+ ...this.tools.map((t) => ({
2795
+ name: t.name,
2796
+ description: t.description,
2797
+ parameters: t.parameters
2798
+ }))
2799
+ );
2800
+ }
2801
+ if (this.mcpServers.length > 0) {
2802
+ try {
2803
+ await this.initializeMCPSchemas();
2804
+ allToolDeclarations.push(
2805
+ ...this.mcpToolSchemas.map((t) => ({
2806
+ name: t.name,
2807
+ description: t.description,
2808
+ parameters: t.parameters
2809
+ }))
2810
+ );
2811
+ } catch (error) {
2812
+ console.warn("MCP initialization failed, skipping MCP tools:", error);
2813
+ }
2814
+ }
2815
+ if (allToolDeclarations.length > 0) {
2816
+ body.tools = [
2817
+ {
2818
+ functionDeclarations: allToolDeclarations
2819
+ }
2820
+ ];
2821
+ body.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
2822
+ }
2823
+ const fetchOnce = async (ver, payload) => {
2824
+ const fn = stream ? "streamGenerateContent" : "generateContent";
2825
+ const alt = stream ? "?alt=sse" : "";
2826
+ const url = `${ENDPOINT_GEMINI_API}/${ver}/models/${model}:${fn}${alt}${alt ? "&" : "?"}key=${this.apiKey}`;
2827
+ return ChatServiceHttpClient.post(url, payload);
2828
+ };
2829
+ const isLite = /flash[-_]lite/.test(model);
2830
+ const isGemma4 = this.isGemma4Model(model);
2831
+ const isGemini25 = /gemini-2\.5/.test(model);
2832
+ const isGemini3Preview = /^gemini-3(?:\.[0-9]+)?-.*preview/.test(model);
2833
+ const requiresV1beta = isLite || isGemma4 || isGemini25 || isGemini3Preview;
2834
+ const firstVer = requiresV1beta ? "v1beta" : "v1";
2835
+ const tryApi = async () => {
2836
+ try {
2837
+ const payload = firstVer === "v1" ? body : this.adaptKeysForApi(body);
2838
+ return await fetchOnce(firstVer, payload);
2839
+ } catch (e) {
2840
+ const looksLikeVersionMismatch = /Unknown name|Cannot find field|404/.test(e?.message || "") || e?.status === 404;
2841
+ if (!requiresV1beta && looksLikeVersionMismatch) {
2842
+ return await fetchOnce("v1beta", this.adaptKeysForApi(body));
2843
+ }
2844
+ throw e;
2845
+ }
2846
+ };
2847
+ try {
2848
+ const res = await tryApi();
2849
+ return res;
2850
+ } catch (error) {
2851
+ if (error.body) {
2852
+ console.error("Gemini API Error Details:", error.body);
2853
+ console.error("Request Body:", JSON.stringify(body, null, 2));
2854
+ }
2855
+ throw error;
2856
+ }
2857
+ }
2858
+ /**
2859
+ * Convert AITuber OnAir vision messages to Gemini format
2860
+ * @param messages Array of vision messages
2861
+ * @returns Gemini formatted vision messages
2862
+ */
2863
+ async convertVisionMessagesToGeminiFormat(messages) {
2864
+ const geminiMessages = [];
2865
+ let currentRole = null;
2866
+ let currentParts = [];
2867
+ for (const msg of messages) {
2868
+ const role = this.mapRoleToGemini(msg.role);
2869
+ if (msg.tool_calls) {
2870
+ for (const call of msg.tool_calls) {
2871
+ geminiMessages.push({
2872
+ role: "model",
2873
+ parts: [
2874
+ {
2875
+ functionCall: {
2876
+ name: call.function.name,
2877
+ args: JSON.parse(call.function.arguments || "{}")
2878
+ }
2879
+ }
2880
+ ]
2881
+ });
2882
+ }
2883
+ continue;
2884
+ }
2885
+ if (msg.role === "tool") {
2886
+ const funcName = msg.name ?? this.callIdMap.get(msg.tool_call_id) ?? "result";
2887
+ geminiMessages.push({
2888
+ role: "user",
2889
+ parts: [
2890
+ {
2891
+ functionResponse: {
2892
+ name: funcName,
2893
+ response: this.normalizeToolResult(
2894
+ this.safeJsonParse(msg.content)
2895
+ )
2896
+ }
2897
+ }
2898
+ ]
2899
+ });
2900
+ continue;
2901
+ }
2902
+ if (role !== currentRole && currentParts.length > 0) {
2903
+ geminiMessages.push({
2904
+ role: currentRole,
2905
+ parts: [...currentParts]
2906
+ });
2907
+ currentParts = [];
2908
+ }
2909
+ currentRole = role;
2910
+ if (typeof msg.content === "string") {
2911
+ currentParts.push({ text: msg.content });
2912
+ } else if (Array.isArray(msg.content)) {
2913
+ for (const block of msg.content) {
2914
+ if (block.type === "text") {
2915
+ currentParts.push({ text: block.text });
2916
+ } else if (block.type === "image_url") {
2917
+ try {
2918
+ const imageResponse = await ChatServiceHttpClient.get(
2919
+ block.image_url.url
2920
+ );
2921
+ const imageBlob = await imageResponse.blob();
2922
+ const base64Data = await this.blobToBase64(imageBlob);
2923
+ currentParts.push({
2924
+ inlineData: {
2925
+ mimeType: imageBlob.type || "image/jpeg",
2926
+ data: base64Data.split(",")[1]
2927
+ // Remove the "data:image/jpeg;base64," prefix
2928
+ }
2929
+ });
2930
+ } catch (error) {
2931
+ console.error("Error processing image:", error);
2932
+ throw new Error(`Failed to process image: ${error.message}`);
2933
+ }
2934
+ }
2935
+ }
2936
+ }
2937
+ }
2938
+ if (currentRole && currentParts.length > 0) {
2939
+ geminiMessages.push({
2940
+ role: currentRole,
2941
+ parts: [...currentParts]
2942
+ });
2943
+ }
2944
+ return geminiMessages;
2945
+ }
2946
+ /**
2947
+ * Convert Blob to Base64 string
2948
+ * @param blob Image blob
2949
+ * @returns Promise with base64 encoded string
2950
+ */
2951
+ blobToBase64(blob) {
2952
+ return new Promise((resolve, reject) => {
2953
+ const reader = new FileReader();
2954
+ reader.onloadend = () => resolve(reader.result);
2955
+ reader.onerror = reject;
2956
+ reader.readAsDataURL(blob);
2957
+ });
2958
+ }
2959
+ /**
2960
+ * Map AITuber OnAir roles to Gemini roles
2961
+ * @param role AITuber OnAir role
2962
+ * @returns Gemini role
2963
+ */
2964
+ mapRoleToGemini(role) {
2965
+ switch (role) {
2966
+ case "system":
2967
+ return "model";
2968
+ // Gemini uses 'model' for system messages
2969
+ case "user":
2970
+ return "user";
2971
+ case "assistant":
2972
+ return "model";
2973
+ default:
2974
+ return "user";
2975
+ }
2976
+ }
2977
+ /* ────────────────────────────────────────────────────────── */
2978
+ /* Convert NDJSON stream to common format */
2979
+ /* ────────────────────────────────────────────────────────── */
2980
+ async parseStream(res, onPartial, model) {
2981
+ const reader = res.body.getReader();
2982
+ const dec = new TextDecoder();
2983
+ const textBlocks = [];
2984
+ const toolBlocks = [];
2985
+ let buf = "";
2986
+ const flush = (payload) => {
2987
+ if (!payload || payload === "[DONE]") return;
2988
+ let obj;
2989
+ try {
2990
+ obj = JSON.parse(payload);
2991
+ } catch {
2992
+ return;
2993
+ }
2994
+ for (const cand of obj.candidates ?? []) {
2995
+ for (const part of cand.content?.parts ?? []) {
2996
+ if (this.shouldExposeTextPart(part, model)) {
2997
+ onPartial(part.text);
2998
+ StreamTextAccumulator.addTextBlock(textBlocks, part.text);
2999
+ }
3000
+ if (part.functionCall) {
3001
+ toolBlocks.push({
3002
+ type: "tool_use",
3003
+ id: this.genUUID(),
3004
+ name: part.functionCall.name,
3005
+ input: part.functionCall.args ?? {}
3006
+ });
3007
+ }
3008
+ if (part.functionResponse) {
3009
+ toolBlocks.push({
3010
+ type: "tool_result",
3011
+ tool_use_id: part.functionResponse.name,
3012
+ content: JSON.stringify(part.functionResponse.response)
3013
+ });
3014
+ }
3015
+ }
3016
+ }
3017
+ };
3018
+ while (true) {
3019
+ const { done, value } = await reader.read();
3020
+ if (done) break;
3021
+ buf += dec.decode(value, { stream: true });
3022
+ let nl;
3023
+ while ((nl = buf.indexOf("\n")) !== -1) {
3024
+ let line = buf.slice(0, nl);
3025
+ buf = buf.slice(nl + 1);
3026
+ if (line.endsWith("\r")) line = line.slice(0, -1);
3027
+ if (!line.trim()) {
3028
+ flush("");
3029
+ continue;
3030
+ }
3031
+ if (line.startsWith("data:")) line = line.slice(5).trim();
3032
+ if (!line) continue;
3033
+ flush(line);
3034
+ }
3035
+ }
3036
+ if (buf) flush(buf);
3037
+ const blocks = [...textBlocks, ...toolBlocks];
3038
+ return {
3039
+ blocks,
3040
+ stop_reason: toolBlocks.some((b) => b.type === "tool_use") ? "tool_use" : "end"
3041
+ };
3042
+ }
3043
+ /* ────────────────────────────────────────────────────────── */
3044
+ /* Convert JSON of non-stream (= generateContent) */
3045
+ /* ────────────────────────────────────────────────────────── */
3046
+ parseOneShot(data, model) {
3047
+ const textBlocks = [];
3048
+ const toolBlocks = [];
3049
+ for (const cand of data.candidates ?? []) {
3050
+ for (const part of cand.content?.parts ?? []) {
3051
+ if (this.shouldExposeTextPart(part, model)) {
3052
+ textBlocks.push({ type: "text", text: part.text });
3053
+ }
3054
+ if (part.functionCall) {
3055
+ toolBlocks.push({
3056
+ type: "tool_use",
3057
+ id: this.genUUID(),
3058
+ name: part.functionCall.name,
3059
+ input: part.functionCall.args ?? {}
3060
+ });
3061
+ }
3062
+ if (part.functionResponse) {
3063
+ toolBlocks.push({
3064
+ type: "tool_result",
3065
+ tool_use_id: part.functionResponse.name,
3066
+ content: JSON.stringify(part.functionResponse.response)
3067
+ });
3068
+ }
3069
+ }
2660
3070
  }
2661
- await processChatWithOptionalTools({
2662
- hasTools: this.tools.length > 0,
2663
- runWithoutTools: async () => {
2664
- const res = await this.callKimi(messages, this.visionModel, true);
2665
- return this.handleStream(res, onPartialResponse);
2666
- },
2667
- runWithTools: () => this.visionChatOnce(messages, true, onPartialResponse),
2668
- onCompleteResponse,
2669
- toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
2670
- });
3071
+ const blocks = [...textBlocks, ...toolBlocks];
3072
+ return {
3073
+ blocks,
3074
+ stop_reason: toolBlocks.some((b) => b.type === "tool_use") ? "tool_use" : "end"
3075
+ };
2671
3076
  }
2672
- /**
2673
- * Process chat messages with tools (text only)
2674
- */
3077
+ /* ────────────────────────────────────────────────────────── */
3078
+ /* chatOnce (text) */
3079
+ /* ────────────────────────────────────────────────────────── */
2675
3080
  async chatOnce(messages, stream = true, onPartialResponse = () => {
2676
3081
  }, maxTokens) {
2677
- const res = await this.callKimi(messages, this.model, stream, maxTokens);
2678
- return this.parseResponse(res, stream, onPartialResponse);
3082
+ const res = await this.callGemini(messages, this.model, stream, maxTokens);
3083
+ return stream ? this.parseStream(res, onPartialResponse, this.model) : this.parseOneShot(await res.json(), this.model);
2679
3084
  }
2680
- /**
2681
- * Process vision chat messages with tools
2682
- */
3085
+ /* ────────────────────────────────────────────────────────── */
3086
+ /* visionChatOnce (images) */
3087
+ /* ────────────────────────────────────────────────────────── */
2683
3088
  async visionChatOnce(messages, stream = false, onPartialResponse = () => {
2684
3089
  }, maxTokens) {
2685
- if (!isKimiVisionModel(this.visionModel)) {
2686
- throw new Error(
2687
- `Model ${this.visionModel} does not support vision capabilities.`
2688
- );
2689
- }
2690
- const res = await this.callKimi(
3090
+ const res = await this.callGemini(
2691
3091
  messages,
2692
3092
  this.visionModel,
2693
3093
  stream,
2694
3094
  maxTokens
2695
3095
  );
2696
- return this.parseResponse(res, stream, onPartialResponse);
2697
- }
2698
- async parseResponse(res, stream, onPartialResponse) {
2699
- return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
2700
- }
2701
- async callKimi(messages, model, stream = false, maxTokens) {
2702
- const body = this.buildRequestBody(messages, model, stream, maxTokens);
2703
- const res = await ChatServiceHttpClient.post(this.endpoint, body, {
2704
- Authorization: `Bearer ${this.apiKey}`
2705
- });
2706
- return res;
2707
- }
2708
- /**
2709
- * Build request body (OpenAI-compatible Chat Completions)
2710
- */
2711
- buildRequestBody(messages, model, stream, maxTokens) {
2712
- const body = {
2713
- model,
2714
- stream,
2715
- messages
2716
- };
2717
- const tokenLimit = maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength);
2718
- if (tokenLimit !== void 0) {
2719
- body.max_tokens = tokenLimit;
2720
- }
2721
- if (this.responseFormat) {
2722
- body.response_format = this.responseFormat;
2723
- }
2724
- const effectiveThinking = this.tools.length > 0 ? { type: "disabled" } : this.thinking;
2725
- if (effectiveThinking) {
2726
- if (this.isSelfHostedEndpoint()) {
2727
- if (effectiveThinking.type === "disabled") {
2728
- body.chat_template_kwargs = { thinking: false };
2729
- }
2730
- } else {
2731
- body.thinking = effectiveThinking;
2732
- }
2733
- }
2734
- const tools = this.buildToolsDefinition();
2735
- if (tools.length > 0) {
2736
- body.tools = tools;
2737
- body.tool_choice = "auto";
2738
- }
2739
- return body;
2740
- }
2741
- isSelfHostedEndpoint() {
2742
- return this.normalizeEndpoint(this.endpoint) !== this.normalizeEndpoint(ENDPOINT_KIMI_CHAT_COMPLETIONS_API);
2743
- }
2744
- normalizeEndpoint(value) {
2745
- return value.replace(/\/+$/, "");
2746
- }
2747
- buildToolsDefinition() {
2748
- return buildOpenAICompatibleTools(this.tools, "chat-completions");
2749
- }
2750
- async handleStream(res, onPartial) {
2751
- return parseOpenAICompatibleTextStream(res, onPartial, {
2752
- onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
2753
- });
3096
+ return stream ? this.parseStream(res, onPartialResponse, this.visionModel) : this.parseOneShot(await res.json(), this.visionModel);
2754
3097
  }
2755
- /**
2756
- * Parse streaming response with tool support
2757
- */
2758
- async parseStream(res, onPartial) {
2759
- return parseOpenAICompatibleToolStream(res, onPartial, {
2760
- onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3098
+ /* ────────────────────────────────────────────────────────── */
3099
+ /* UUID helper */
3100
+ /* ────────────────────────────────────────────────────────── */
3101
+ genUUID() {
3102
+ return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
3103
+ const r = Math.random() * 16 | 0;
3104
+ const v = c === "x" ? r : r & 3 | 8;
3105
+ return v.toString(16);
2761
3106
  });
2762
3107
  }
2763
- /**
2764
- * Parse non-streaming response
2765
- */
2766
- parseOneShot(data) {
2767
- return parseOpenAICompatibleOneShot(data);
2768
- }
2769
3108
  };
2770
3109
 
2771
- // src/services/providers/kimi/KimiChatServiceProvider.ts
2772
- var KimiChatServiceProvider = class {
3110
+ // src/services/providers/gemini/GeminiChatServiceProvider.ts
3111
+ var GeminiChatServiceProvider = class {
2773
3112
  /**
2774
3113
  * Create a chat service instance
3114
+ * @param options Service options
3115
+ * @returns GeminiChatService instance
2775
3116
  */
2776
3117
  createChatService(options) {
2777
- const endpoint = this.resolveEndpoint(options);
2778
- const model = options.model || this.getDefaultModel();
2779
3118
  const visionModel = resolveVisionModel({
2780
- model,
3119
+ model: options.model,
2781
3120
  visionModel: options.visionModel,
2782
3121
  defaultModel: this.getDefaultModel(),
2783
- defaultVisionModel: this.getDefaultVisionModel(),
2784
- supportsVisionForModel: (visionModel2) => this.supportsVisionForModel(visionModel2),
2785
- validate: "explicit"
3122
+ defaultVisionModel: this.getDefaultModel(),
3123
+ supportsVisionForModel: (model) => this.supportsVisionForModel(model),
3124
+ validate: "resolved"
2786
3125
  });
2787
- const tools = options.tools;
2788
- const defaultThinking = options.thinking ?? { type: "enabled" };
2789
- const thinking = tools && tools.length > 0 ? { type: "disabled" } : defaultThinking;
2790
- return new KimiChatService(
3126
+ return new GeminiChatService(
2791
3127
  options.apiKey,
2792
- model,
3128
+ options.model || this.getDefaultModel(),
2793
3129
  visionModel,
2794
- tools,
2795
- endpoint,
2796
- options.responseLength,
2797
- options.responseFormat,
2798
- thinking
3130
+ options.tools || [],
3131
+ options.mcpServers || [],
3132
+ options.responseLength
2799
3133
  );
2800
3134
  }
2801
3135
  /**
2802
3136
  * Get the provider name
3137
+ * @returns Provider name ('gemini')
2803
3138
  */
2804
3139
  getProviderName() {
2805
- return "kimi";
3140
+ return "gemini";
2806
3141
  }
2807
3142
  /**
2808
3143
  * Get the list of supported models
3144
+ * @returns Array of supported model names
2809
3145
  */
2810
3146
  getSupportedModels() {
2811
- return [MODEL_KIMI_K2_6, MODEL_KIMI_K2_5];
3147
+ return [...GEMINI_RECOMMENDED_MODELS];
2812
3148
  }
2813
3149
  /**
2814
3150
  * Get the default model
3151
+ * @returns Default model name
2815
3152
  */
2816
3153
  getDefaultModel() {
2817
- return MODEL_KIMI_K2_6;
3154
+ return MODEL_GEMINI_3_1_FLASH_LITE;
2818
3155
  }
2819
3156
  /**
2820
- * Get the default vision model
3157
+ * Check if this provider supports vision (image processing)
3158
+ * @returns Vision support status (true)
2821
3159
  */
2822
- getDefaultVisionModel() {
2823
- return MODEL_KIMI_K2_6;
3160
+ supportsVision() {
3161
+ return this.getVisionSupportLevel() !== "unsupported";
3162
+ }
3163
+ getVisionSupportLevel() {
3164
+ return "supported";
3165
+ }
3166
+ /**
3167
+ * Check if a specific model supports vision capabilities
3168
+ * @param model The model name to check
3169
+ * @returns True if the model supports vision, false otherwise
3170
+ */
3171
+ supportsVisionForModel(model) {
3172
+ return GEMINI_VISION_SUPPORTED_MODELS.includes(model);
3173
+ }
3174
+ getVisionSupportLevelForModel(model) {
3175
+ return this.supportsVisionForModel(model) ? "supported" : "unsupported";
3176
+ }
3177
+ };
3178
+
3179
+ // src/services/providers/geminiNano/GeminiNanoChatService.ts
3180
+ function getLanguageModelAPI() {
3181
+ if (typeof globalThis !== "undefined" && "LanguageModel" in globalThis) {
3182
+ return globalThis.LanguageModel;
3183
+ }
3184
+ return void 0;
3185
+ }
3186
+ var GeminiNanoChatService = class {
3187
+ constructor(options = {}) {
3188
+ this.provider = "gemini-nano";
3189
+ this.expectedInputLanguages = options.expectedInputLanguages ?? ["ja"];
3190
+ this.expectedOutputLanguages = options.expectedOutputLanguages ?? ["ja"];
3191
+ this._responseLength = options.responseLength;
3192
+ }
3193
+ getModel() {
3194
+ return MODEL_GEMINI_NANO;
3195
+ }
3196
+ getVisionModel() {
3197
+ return MODEL_GEMINI_NANO;
3198
+ }
3199
+ /**
3200
+ * Process chat messages using Gemini Nano.
3201
+ * Non-streaming: calls onPartialResponse once with the full response,
3202
+ * then calls onCompleteResponse.
3203
+ */
3204
+ async processChat(messages, onPartialResponse, onCompleteResponse) {
3205
+ const response = await this.generateResponse(messages);
3206
+ onPartialResponse(response);
3207
+ await onCompleteResponse(response);
3208
+ }
3209
+ async processVisionChat(_messages, _onPartialResponse, _onCompleteResponse) {
3210
+ throw new Error("Gemini Nano does not support vision capabilities.");
3211
+ }
3212
+ async chatOnce(messages, _stream = false, onPartialResponse = () => {
3213
+ }, _maxTokens) {
3214
+ const response = await this.generateResponse(messages);
3215
+ onPartialResponse(response);
3216
+ return {
3217
+ blocks: [{ type: "text", text: response }],
3218
+ stop_reason: "end"
3219
+ };
3220
+ }
3221
+ async visionChatOnce(_messages, _stream = false, _onPartialResponse = () => {
3222
+ }, _maxTokens) {
3223
+ throw new Error("Gemini Nano does not support vision capabilities.");
2824
3224
  }
2825
3225
  /**
2826
- * Check if this provider supports vision
3226
+ * Core logic: extract system prompt, manage session, call prompt().
2827
3227
  */
2828
- supportsVision() {
2829
- return this.getVisionSupportLevel() !== "unsupported";
2830
- }
2831
- getVisionSupportLevel() {
2832
- return "supported";
3228
+ async generateResponse(messages) {
3229
+ const api = getLanguageModelAPI();
3230
+ if (!api) {
3231
+ throw new Error(
3232
+ "Gemini Nano is not available in this environment. Chrome 138+ with Prompt API enabled is required."
3233
+ );
3234
+ }
3235
+ const availability = await api.availability();
3236
+ if (availability !== "available" && availability !== "downloadable") {
3237
+ throw new Error(
3238
+ `Gemini Nano Prompt API is not ready in this environment. LanguageModel.availability() returned "${availability}". Expected "available" or "downloadable".`
3239
+ );
3240
+ }
3241
+ const systemMessages = messages.filter((m) => m.role === "system");
3242
+ const systemPrompt = systemMessages.map((m) => m.content).join("\n");
3243
+ const conversationMessages = messages.filter((m) => m.role !== "system").slice(-GEMINI_NANO_MAX_CONTEXT_MESSAGES);
3244
+ const lastUserMessage = [...conversationMessages].reverse().find((m) => m.role === "user");
3245
+ if (!lastUserMessage) {
3246
+ throw new Error("No user message found in the provided messages.");
3247
+ }
3248
+ const session = await this.createSession(
3249
+ api,
3250
+ systemPrompt,
3251
+ conversationMessages
3252
+ );
3253
+ try {
3254
+ return await session.prompt(lastUserMessage.content);
3255
+ } finally {
3256
+ try {
3257
+ session.destroy();
3258
+ } catch {
3259
+ }
3260
+ }
2833
3261
  }
2834
3262
  /**
2835
- * Check if a specific model supports vision capabilities
3263
+ * Create a new LanguageModel session with system prompt and context history.
3264
+ * Context history (excluding the last user message) is embedded in the system prompt.
2836
3265
  */
2837
- supportsVisionForModel(model) {
2838
- return isKimiVisionModel(model);
2839
- }
2840
- getVisionSupportLevelForModel(model) {
2841
- return this.supportsVisionForModel(model) ? "supported" : "unsupported";
3266
+ async createSession(api, systemPrompt, contextHistory) {
3267
+ let prompt = this.buildSystemPrompt(systemPrompt);
3268
+ const historyMessages = contextHistory.slice(0, -1);
3269
+ if (historyMessages.length > 0) {
3270
+ const history = historyMessages.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n");
3271
+ prompt += "\n\nThe following is the prior conversation history. Use it as context for your response:\n" + history;
3272
+ }
3273
+ return api.create({
3274
+ systemPrompt: prompt,
3275
+ expectedInputs: [
3276
+ { type: "text", languages: this.expectedInputLanguages }
3277
+ ],
3278
+ expectedOutputs: [
3279
+ { type: "text", languages: this.expectedOutputLanguages }
3280
+ ]
3281
+ });
2842
3282
  }
2843
- resolveEndpoint(options) {
2844
- if (options.endpoint) {
2845
- return this.normalizeEndpoint(options.endpoint);
3283
+ buildSystemPrompt(systemPrompt) {
3284
+ const promptParts = [];
3285
+ if (systemPrompt) {
3286
+ promptParts.push(systemPrompt);
2846
3287
  }
2847
- if (options.baseUrl) {
2848
- const baseUrl = this.normalizeEndpoint(options.baseUrl);
2849
- if (baseUrl.endsWith("/chat/completions")) {
2850
- return baseUrl;
2851
- }
2852
- return `${baseUrl}/chat/completions`;
3288
+ const lengthInstruction = this.getResponseLengthInstruction();
3289
+ if (lengthInstruction) {
3290
+ promptParts.push(lengthInstruction);
2853
3291
  }
2854
- return ENDPOINT_KIMI_CHAT_COMPLETIONS_API;
3292
+ return promptParts.join("\n\n");
2855
3293
  }
2856
- normalizeEndpoint(value) {
2857
- return value.replace(/\/+$/, "");
3294
+ getResponseLengthInstruction() {
3295
+ if (!this._responseLength) {
3296
+ return void 0;
3297
+ }
3298
+ const maxTokens = MAX_TOKENS_BY_LENGTH[this._responseLength];
3299
+ if (!maxTokens) {
3300
+ return void 0;
3301
+ }
3302
+ return `Please keep your response concise, within approximately ${maxTokens} tokens.`;
2858
3303
  }
2859
3304
  };
2860
3305
 
2861
- // src/services/providers/openai/responsesParser.ts
2862
- async function parseOpenAIResponsesStream(res, onPartial) {
2863
- const reader = res.body.getReader();
2864
- const dec = new TextDecoder();
2865
- const textBlocks = [];
2866
- const toolCallsMap = /* @__PURE__ */ new Map();
2867
- let responseStatus;
2868
- let incompleteDetails;
2869
- let usage;
2870
- let buf = "";
2871
- while (true) {
2872
- const { done, value } = await reader.read();
2873
- if (done) break;
2874
- buf += dec.decode(value, { stream: true });
2875
- let eventType = "";
2876
- let eventData = "";
2877
- const lines = buf.split("\n");
2878
- buf = lines.pop() || "";
2879
- for (let i = 0; i < lines.length; i++) {
2880
- const line = lines[i].trim();
2881
- if (line.startsWith("event:")) {
2882
- eventType = line.slice(6).trim();
2883
- } else if (line.startsWith("data:")) {
2884
- eventData = line.slice(5).trim();
2885
- } else if (line === "" && eventType && eventData) {
2886
- try {
2887
- const json = JSON.parse(eventData);
2888
- handleResponsesSSEEvent(
2889
- eventType,
2890
- json,
2891
- onPartial,
2892
- textBlocks,
2893
- toolCallsMap,
2894
- (metadata) => {
2895
- if (metadata.responseStatus !== void 0) {
2896
- responseStatus = metadata.responseStatus;
2897
- }
2898
- if (metadata.incompleteDetails !== void 0) {
2899
- incompleteDetails = metadata.incompleteDetails;
2900
- }
2901
- if (metadata.usage !== void 0) {
2902
- usage = metadata.usage;
2903
- }
2904
- }
2905
- );
2906
- } catch {
2907
- console.warn("Failed to parse SSE data:", eventData);
2908
- }
2909
- eventType = "";
2910
- eventData = "";
2911
- }
2912
- }
3306
+ // src/services/providers/geminiNano/GeminiNanoChatServiceProvider.ts
3307
+ var GeminiNanoChatServiceProvider = class {
3308
+ createChatService(options) {
3309
+ return new GeminiNanoChatService({
3310
+ expectedInputLanguages: options.expectedInputLanguages,
3311
+ expectedOutputLanguages: options.expectedOutputLanguages,
3312
+ responseLength: options.responseLength
3313
+ });
2913
3314
  }
2914
- const toolBlocks = Array.from(toolCallsMap.values()).map(
2915
- (tool) => ({
2916
- type: "tool_use",
2917
- id: tool.id,
2918
- name: tool.name,
2919
- input: tool.input || {}
2920
- })
2921
- );
2922
- return {
2923
- blocks: [...textBlocks, ...toolBlocks],
2924
- stop_reason: toolBlocks.length ? "tool_use" : "end",
2925
- truncated: responseStatus === "incomplete",
2926
- response_status: responseStatus,
2927
- incomplete_details: incompleteDetails,
2928
- usage
2929
- };
2930
- }
2931
- function handleResponsesSSEEvent(eventType, data, onPartial, textBlocks, toolCallsMap, onMetadata) {
2932
- switch (eventType) {
2933
- case "response.output_item.added":
2934
- if (data.item?.type === "message" && Array.isArray(data.item.content)) {
2935
- data.item.content.forEach((c) => {
2936
- if (c.type === "output_text" && c.text) {
2937
- onPartial(c.text);
2938
- StreamTextAccumulator.append(textBlocks, c.text);
2939
- }
2940
- });
2941
- } else if (data.item?.type === "function_call") {
2942
- toolCallsMap.set(data.item.id, {
2943
- id: data.item.id,
2944
- name: data.item.name,
2945
- input: data.item.arguments ? JSON.parse(data.item.arguments) : {}
2946
- });
2947
- }
2948
- break;
2949
- case "response.content_part.added":
2950
- if (data.part?.type === "output_text" && typeof data.part.text === "string") {
2951
- onPartial(data.part.text);
2952
- StreamTextAccumulator.append(textBlocks, data.part.text);
2953
- }
2954
- break;
2955
- case "response.output_text.delta":
2956
- case "response.content_part.delta": {
2957
- const deltaText = typeof data.delta === "string" ? data.delta : data.delta?.text ?? "";
2958
- if (deltaText) {
2959
- onPartial(deltaText);
2960
- StreamTextAccumulator.append(textBlocks, deltaText);
2961
- }
2962
- break;
2963
- }
2964
- case "response.output_text.done":
2965
- case "response.content_part.done":
2966
- case "response.reasoning.started":
2967
- case "response.reasoning.delta":
2968
- case "response.reasoning.done":
2969
- break;
2970
- case "response.completed":
2971
- onMetadata(extractResponsesMetadata(data, "completed"));
2972
- break;
2973
- case "response.incomplete":
2974
- onMetadata(extractResponsesMetadata(data, "incomplete"));
2975
- break;
2976
- default:
2977
- break;
3315
+ getProviderName() {
3316
+ return "gemini-nano";
2978
3317
  }
2979
- }
2980
- function extractResponsesMetadata(data, fallbackStatus) {
2981
- const response = data?.response ?? data;
2982
- return {
2983
- responseStatus: response?.status ?? fallbackStatus,
2984
- incompleteDetails: response?.incomplete_details ?? null,
2985
- usage: response?.usage
2986
- };
2987
- }
2988
- function parseOpenAIResponsesOneShot(data) {
2989
- const blocks = [];
2990
- if (data.output && Array.isArray(data.output)) {
2991
- data.output.forEach((outputItem) => {
2992
- if (outputItem.type === "message" && outputItem.content) {
2993
- outputItem.content.forEach((content) => {
2994
- if (content.type === "output_text" && content.text) {
2995
- blocks.push({ type: "text", text: content.text });
2996
- }
2997
- });
2998
- }
2999
- if (outputItem.type === "function_call") {
3000
- blocks.push({
3001
- type: "tool_use",
3002
- id: outputItem.id,
3003
- name: outputItem.name,
3004
- input: outputItem.arguments ? JSON.parse(outputItem.arguments) : {}
3005
- });
3006
- }
3007
- });
3318
+ getSupportedModels() {
3319
+ return [MODEL_GEMINI_NANO];
3320
+ }
3321
+ getDefaultModel() {
3322
+ return MODEL_GEMINI_NANO;
3323
+ }
3324
+ supportsVision() {
3325
+ return false;
3326
+ }
3327
+ getVisionSupportLevel() {
3328
+ return "unsupported";
3008
3329
  }
3009
- return {
3010
- blocks,
3011
- stop_reason: blocks.some((b) => b.type === "tool_use") ? "tool_use" : "end",
3012
- truncated: data?.status === "incomplete",
3013
- response_status: data?.status,
3014
- incomplete_details: data?.incomplete_details ?? null,
3015
- usage: data?.usage
3016
- };
3017
- }
3018
-
3019
- // src/services/providers/openai/OpenAIChatService.ts
3020
- var GPT5_RESPONSE_LENGTH_MIN_TOKENS = {
3021
- [CHAT_RESPONSE_LENGTH.VERY_SHORT]: 800,
3022
- [CHAT_RESPONSE_LENGTH.SHORT]: 1200,
3023
- [CHAT_RESPONSE_LENGTH.MEDIUM]: 2e3,
3024
- [CHAT_RESPONSE_LENGTH.LONG]: 3e3,
3025
- [CHAT_RESPONSE_LENGTH.VERY_LONG]: 8e3,
3026
- [CHAT_RESPONSE_LENGTH.DEEP]: 25e3
3027
- };
3028
- var GPT5_REASONING_MIN_TOKENS = {
3029
- none: 1200,
3030
- minimal: 1600,
3031
- low: 2500,
3032
- medium: 4e3,
3033
- high: 8e3,
3034
- xhigh: 12e3
3035
3330
  };
3036
- var OpenAIChatService = class {
3331
+
3332
+ // src/services/providers/kimi/KimiChatService.ts
3333
+ var KimiChatService = class {
3037
3334
  /**
3038
3335
  * Constructor
3039
- * @param apiKey OpenAI API key
3336
+ * @param apiKey Kimi API key
3040
3337
  * @param model Name of the model to use
3041
3338
  * @param visionModel Name of the vision model
3042
3339
  */
3043
- constructor(apiKey, model = MODEL_GPT_4O_MINI, visionModel = MODEL_GPT_4O_MINI, tools, endpoint = ENDPOINT_OPENAI_CHAT_COMPLETIONS_API, mcpServers = [], responseLength, verbosity, reasoning_effort, enableReasoningSummary = false, provider = "openai", validateVisionModel = true) {
3044
- this.provider = provider;
3340
+ constructor(apiKey, model = MODEL_KIMI_K2_6, visionModel = MODEL_KIMI_K2_6, tools, endpoint = ENDPOINT_KIMI_CHAT_COMPLETIONS_API, responseLength, responseFormat, thinking) {
3341
+ /** Provider name */
3342
+ this.provider = "kimi";
3045
3343
  this.apiKey = apiKey;
3046
3344
  this.model = model;
3047
3345
  this.tools = tools || [];
3048
3346
  this.endpoint = endpoint;
3049
- this.mcpServers = mcpServers;
3050
3347
  this.responseLength = responseLength;
3051
- this.verbosity = verbosity;
3052
- this.reasoning_effort = reasoning_effort;
3053
- this.enableReasoningSummary = enableReasoningSummary;
3054
- if (validateVisionModel && !VISION_SUPPORTED_MODELS.includes(visionModel)) {
3055
- throw new Error(
3056
- `Model ${visionModel} does not support vision capabilities.`
3057
- );
3058
- }
3348
+ this.responseFormat = responseFormat;
3349
+ this.thinking = thinking ?? { type: "enabled" };
3059
3350
  this.visionModel = visionModel;
3060
3351
  }
3061
3352
  /**
3062
3353
  * Get the current model name
3063
- * @returns Model name
3064
3354
  */
3065
3355
  getModel() {
3066
3356
  return this.model;
3067
3357
  }
3068
3358
  /**
3069
3359
  * Get the current vision model name
3070
- * @returns Vision model name
3071
3360
  */
3072
3361
  getVisionModel() {
3073
3362
  return this.visionModel;
3074
3363
  }
3075
3364
  /**
3076
3365
  * Process chat messages
3077
- * @param messages Array of messages to send
3078
- * @param onPartialResponse Callback to receive each part of streaming response
3079
- * @param onCompleteResponse Callback to execute when response is complete
3080
3366
  */
3081
3367
  async processChat(messages, onPartialResponse, onCompleteResponse) {
3082
3368
  await processChatWithOptionalTools({
3083
3369
  hasTools: this.tools.length > 0,
3084
3370
  runWithoutTools: async () => {
3085
- const res = await this.callOpenAI(messages, this.model, true);
3086
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
3087
- try {
3088
- if (isResponsesAPI) {
3089
- const result = await parseOpenAIResponsesStream(
3090
- res,
3091
- onPartialResponse
3092
- );
3093
- return StreamTextAccumulator.getFullText(result.blocks);
3094
- }
3095
- return this.handleStream(res, onPartialResponse);
3096
- } catch (error) {
3097
- console.error("[processChat] Error in streaming/completion:", error);
3098
- throw error;
3099
- }
3371
+ const res = await this.callKimi(messages, this.model, true);
3372
+ return this.handleStream(res, onPartialResponse);
3100
3373
  },
3101
3374
  runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
3102
3375
  onCompleteResponse,
@@ -3105,68 +3378,43 @@ If it's in another language, summarize in that language.
3105
3378
  }
3106
3379
  /**
3107
3380
  * Process chat messages with images
3108
- * @param messages Array of messages to send (including images)
3109
- * @param onPartialResponse Callback to receive each part of streaming response
3110
- * @param onCompleteResponse Callback to execute when response is complete
3111
- * @throws Error if the selected model doesn't support vision
3112
3381
  */
3113
3382
  async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
3114
- try {
3115
- await processChatWithOptionalTools({
3116
- hasTools: this.tools.length > 0,
3117
- runWithoutTools: async () => {
3118
- const res = await this.callOpenAI(messages, this.visionModel, true);
3119
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
3120
- try {
3121
- if (isResponsesAPI) {
3122
- const result = await parseOpenAIResponsesStream(
3123
- res,
3124
- onPartialResponse
3125
- );
3126
- return StreamTextAccumulator.getFullText(result.blocks);
3127
- }
3128
- return this.handleStream(res, onPartialResponse);
3129
- } catch (streamError) {
3130
- console.error(
3131
- "[processVisionChat] Error in streaming/completion:",
3132
- streamError
3133
- );
3134
- throw streamError;
3135
- }
3136
- },
3137
- runWithTools: () => this.visionChatOnce(messages, true, onPartialResponse),
3138
- onCompleteResponse,
3139
- toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
3140
- });
3141
- } catch (error) {
3142
- console.error("Error in processVisionChat:", error);
3143
- throw error;
3383
+ if (!isKimiVisionModel(this.visionModel)) {
3384
+ throw new Error(
3385
+ `Model ${this.visionModel} does not support vision capabilities.`
3386
+ );
3144
3387
  }
3388
+ await processChatWithOptionalTools({
3389
+ hasTools: this.tools.length > 0,
3390
+ runWithoutTools: async () => {
3391
+ const res = await this.callKimi(messages, this.visionModel, true);
3392
+ return this.handleStream(res, onPartialResponse);
3393
+ },
3394
+ runWithTools: () => this.visionChatOnce(messages, true, onPartialResponse),
3395
+ onCompleteResponse,
3396
+ toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
3397
+ });
3145
3398
  }
3146
3399
  /**
3147
3400
  * Process chat messages with tools (text only)
3148
- * @param messages Array of messages to send
3149
- * @param stream Whether to use streaming
3150
- * @param onPartialResponse Callback for partial responses
3151
- * @param maxTokens Maximum tokens for response (optional)
3152
- * @returns Tool chat completion
3153
3401
  */
3154
3402
  async chatOnce(messages, stream = true, onPartialResponse = () => {
3155
3403
  }, maxTokens) {
3156
- const res = await this.callOpenAI(messages, this.model, stream, maxTokens);
3404
+ const res = await this.callKimi(messages, this.model, stream, maxTokens);
3157
3405
  return this.parseResponse(res, stream, onPartialResponse);
3158
3406
  }
3159
3407
  /**
3160
3408
  * Process vision chat messages with tools
3161
- * @param messages Array of messages to send (including images)
3162
- * @param stream Whether to use streaming
3163
- * @param onPartialResponse Callback for partial responses
3164
- * @param maxTokens Maximum tokens for response (optional)
3165
- * @returns Tool chat completion
3166
3409
  */
3167
3410
  async visionChatOnce(messages, stream = false, onPartialResponse = () => {
3168
3411
  }, maxTokens) {
3169
- const res = await this.callOpenAI(
3412
+ if (!isKimiVisionModel(this.visionModel)) {
3413
+ throw new Error(
3414
+ `Model ${this.visionModel} does not support vision capabilities.`
3415
+ );
3416
+ }
3417
+ const res = await this.callKimi(
3170
3418
  messages,
3171
3419
  this.visionModel,
3172
3420
  stream,
@@ -3174,205 +3422,258 @@ If it's in another language, summarize in that language.
3174
3422
  );
3175
3423
  return this.parseResponse(res, stream, onPartialResponse);
3176
3424
  }
3177
- /**
3178
- * Parse response based on endpoint type
3179
- */
3180
3425
  async parseResponse(res, stream, onPartialResponse) {
3181
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
3182
- if (isResponsesAPI) {
3183
- return stream ? parseOpenAIResponsesStream(res, onPartialResponse) : parseOpenAIResponsesOneShot(await res.json());
3184
- }
3185
3426
  return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
3186
3427
  }
3187
- async callOpenAI(messages, model, stream = false, maxTokens) {
3428
+ async callKimi(messages, model, stream = false, maxTokens) {
3188
3429
  const body = this.buildRequestBody(messages, model, stream, maxTokens);
3189
- const headers = {};
3190
- const shouldSendAuthorization = this.provider !== "openai-compatible" || this.apiKey.trim() !== "";
3191
- if (shouldSendAuthorization) {
3192
- headers.Authorization = `Bearer ${this.apiKey}`;
3193
- }
3194
- const res = await ChatServiceHttpClient.post(this.endpoint, body, headers);
3430
+ const res = await ChatServiceHttpClient.post(this.endpoint, body, {
3431
+ Authorization: `Bearer ${this.apiKey}`
3432
+ });
3195
3433
  return res;
3196
3434
  }
3197
3435
  /**
3198
- * Build request body based on the endpoint type
3436
+ * Build request body (OpenAI-compatible Chat Completions)
3437
+ */
3438
+ buildRequestBody(messages, model, stream, maxTokens) {
3439
+ const body = {
3440
+ model,
3441
+ stream,
3442
+ messages
3443
+ };
3444
+ const tokenLimit = maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength);
3445
+ if (tokenLimit !== void 0) {
3446
+ body.max_tokens = tokenLimit;
3447
+ }
3448
+ if (this.responseFormat) {
3449
+ body.response_format = this.responseFormat;
3450
+ }
3451
+ const effectiveThinking = this.tools.length > 0 ? { type: "disabled" } : this.thinking;
3452
+ if (effectiveThinking) {
3453
+ if (this.isSelfHostedEndpoint()) {
3454
+ if (effectiveThinking.type === "disabled") {
3455
+ body.chat_template_kwargs = { thinking: false };
3456
+ }
3457
+ } else {
3458
+ body.thinking = effectiveThinking;
3459
+ }
3460
+ }
3461
+ const tools = this.buildToolsDefinition();
3462
+ if (tools.length > 0) {
3463
+ body.tools = tools;
3464
+ body.tool_choice = "auto";
3465
+ }
3466
+ return body;
3467
+ }
3468
+ isSelfHostedEndpoint() {
3469
+ return this.normalizeEndpoint(this.endpoint) !== this.normalizeEndpoint(ENDPOINT_KIMI_CHAT_COMPLETIONS_API);
3470
+ }
3471
+ normalizeEndpoint(value) {
3472
+ return value.replace(/\/+$/, "");
3473
+ }
3474
+ buildToolsDefinition() {
3475
+ return buildOpenAICompatibleTools(this.tools, "chat-completions");
3476
+ }
3477
+ async handleStream(res, onPartial) {
3478
+ return parseOpenAICompatibleTextStream(res, onPartial, {
3479
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3480
+ });
3481
+ }
3482
+ /**
3483
+ * Parse streaming response with tool support
3484
+ */
3485
+ async parseStream(res, onPartial) {
3486
+ return parseOpenAICompatibleToolStream(res, onPartial, {
3487
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3488
+ });
3489
+ }
3490
+ /**
3491
+ * Parse non-streaming response
3492
+ */
3493
+ parseOneShot(data) {
3494
+ return parseOpenAICompatibleOneShot(data);
3495
+ }
3496
+ };
3497
+
3498
+ // src/services/providers/kimi/KimiChatServiceProvider.ts
3499
+ var KimiChatServiceProvider = class {
3500
+ /**
3501
+ * Create a chat service instance
3502
+ */
3503
+ createChatService(options) {
3504
+ const endpoint = this.resolveEndpoint(options);
3505
+ const model = options.model || this.getDefaultModel();
3506
+ const visionModel = resolveVisionModel({
3507
+ model,
3508
+ visionModel: options.visionModel,
3509
+ defaultModel: this.getDefaultModel(),
3510
+ defaultVisionModel: this.getDefaultVisionModel(),
3511
+ supportsVisionForModel: (visionModel2) => this.supportsVisionForModel(visionModel2),
3512
+ validate: "explicit"
3513
+ });
3514
+ const tools = options.tools;
3515
+ const defaultThinking = options.thinking ?? { type: "enabled" };
3516
+ const thinking = tools && tools.length > 0 ? { type: "disabled" } : defaultThinking;
3517
+ return new KimiChatService(
3518
+ options.apiKey,
3519
+ model,
3520
+ visionModel,
3521
+ tools,
3522
+ endpoint,
3523
+ options.responseLength,
3524
+ options.responseFormat,
3525
+ thinking
3526
+ );
3527
+ }
3528
+ /**
3529
+ * Get the provider name
3530
+ */
3531
+ getProviderName() {
3532
+ return "kimi";
3533
+ }
3534
+ /**
3535
+ * Get the list of supported models
3199
3536
  */
3200
- buildRequestBody(messages, model, stream, maxTokens) {
3201
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
3202
- this.validateMCPCompatibility();
3203
- const body = {
3204
- model,
3205
- stream
3206
- };
3207
- const tokenLimit = this.resolveTokenLimit(model, maxTokens);
3208
- if (isResponsesAPI) {
3209
- if (tokenLimit !== void 0) {
3210
- body.max_output_tokens = tokenLimit;
3211
- }
3212
- } else {
3213
- if (tokenLimit !== void 0) {
3214
- if (this.provider === "openai-compatible") {
3215
- body.max_tokens = tokenLimit;
3216
- } else {
3217
- body.max_completion_tokens = tokenLimit;
3218
- }
3219
- }
3220
- }
3221
- if (isResponsesAPI) {
3222
- body.input = this.cleanMessagesForResponsesAPI(messages);
3223
- } else {
3224
- body.messages = messages;
3225
- }
3226
- if (isGPT5Model(model)) {
3227
- if (isResponsesAPI) {
3228
- if (this.reasoning_effort) {
3229
- body.reasoning = {
3230
- ...body.reasoning,
3231
- effort: this.reasoning_effort
3232
- };
3233
- if (this.enableReasoningSummary) {
3234
- body.reasoning.summary = "auto";
3235
- }
3236
- }
3237
- if (this.verbosity) {
3238
- body.text = {
3239
- ...body.text,
3240
- format: { type: "text" },
3241
- verbosity: this.verbosity
3242
- };
3243
- }
3244
- } else {
3245
- if (this.reasoning_effort) {
3246
- body.reasoning_effort = this.reasoning_effort;
3247
- }
3248
- if (this.verbosity) {
3249
- body.verbosity = this.verbosity;
3250
- }
3251
- }
3252
- }
3253
- const tools = this.buildToolsDefinition();
3254
- if (tools.length > 0) {
3255
- body.tools = tools;
3256
- if (!isResponsesAPI) {
3257
- body.tool_choice = "auto";
3258
- }
3259
- }
3260
- return body;
3261
- }
3262
- resolveTokenLimit(model, maxTokens) {
3263
- if (maxTokens !== void 0) {
3264
- return maxTokens;
3265
- }
3266
- const baseTokenLimit = this.provider === "openai-compatible" ? this.responseLength !== void 0 ? getMaxTokensForResponseLength(this.responseLength) : void 0 : getMaxTokensForResponseLength(this.responseLength);
3267
- if (this.provider !== "openai" || !isGPT5Model(model) || this.responseLength === void 0) {
3268
- return baseTokenLimit;
3269
- }
3270
- const effectiveReasoningEffort = this.reasoning_effort ?? getDefaultReasoningEffortForGPT5Model(model);
3271
- return Math.max(
3272
- baseTokenLimit ?? 0,
3273
- GPT5_RESPONSE_LENGTH_MIN_TOKENS[this.responseLength],
3274
- GPT5_REASONING_MIN_TOKENS[effectiveReasoningEffort]
3275
- );
3537
+ getSupportedModels() {
3538
+ return [MODEL_KIMI_K2_6, MODEL_KIMI_K2_5];
3276
3539
  }
3277
3540
  /**
3278
- * Validate MCP servers compatibility with the current endpoint
3541
+ * Get the default model
3279
3542
  */
3280
- validateMCPCompatibility() {
3281
- if (this.mcpServers.length > 0 && this.endpoint === ENDPOINT_OPENAI_CHAT_COMPLETIONS_API) {
3282
- throw new Error(
3283
- `MCP servers are not supported with Chat Completions API. Current endpoint: ${this.endpoint}. Please use OpenAI Responses API endpoint: ${ENDPOINT_OPENAI_RESPONSES_API}. MCP tools are only available in the Responses API endpoint.`
3284
- );
3285
- }
3543
+ getDefaultModel() {
3544
+ return MODEL_KIMI_K2_6;
3286
3545
  }
3287
3546
  /**
3288
- * Clean messages for Responses API (remove timestamp and other extra properties)
3547
+ * Get the default vision model
3289
3548
  */
3290
- cleanMessagesForResponsesAPI(messages) {
3291
- return messages.map((msg) => {
3292
- const role = msg.role === "tool" ? "user" : msg.role;
3293
- const cleanMsg = {
3294
- role
3295
- };
3296
- if (typeof msg.content === "string") {
3297
- cleanMsg.content = msg.content;
3298
- } else if (Array.isArray(msg.content)) {
3299
- cleanMsg.content = msg.content.map((block) => {
3300
- if (block.type === "text") {
3301
- return {
3302
- type: "input_text",
3303
- text: block.text
3304
- };
3305
- } else if (block.type === "image_url") {
3306
- return {
3307
- type: "input_image",
3308
- image_url: block.image_url.url
3309
- // Extract the URL string directly
3310
- };
3311
- }
3312
- return block;
3313
- });
3314
- } else {
3315
- cleanMsg.content = msg.content;
3316
- }
3317
- return cleanMsg;
3318
- });
3549
+ getDefaultVisionModel() {
3550
+ return MODEL_KIMI_K2_6;
3319
3551
  }
3320
3552
  /**
3321
- * Build tools definition based on the endpoint type
3553
+ * Check if this provider supports vision
3322
3554
  */
3323
- buildToolsDefinition() {
3324
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
3325
- const toolDefs = [];
3326
- if (this.tools.length > 0) {
3327
- toolDefs.push(
3328
- ...buildOpenAICompatibleTools(
3329
- this.tools,
3330
- isResponsesAPI ? "responses" : "chat-completions"
3331
- )
3332
- );
3333
- }
3334
- if (this.mcpServers.length > 0 && isResponsesAPI) {
3335
- toolDefs.push(...this.buildMCPToolsDefinition());
3336
- }
3337
- return toolDefs;
3555
+ supportsVision() {
3556
+ return this.getVisionSupportLevel() !== "unsupported";
3557
+ }
3558
+ getVisionSupportLevel() {
3559
+ return "supported";
3338
3560
  }
3339
3561
  /**
3340
- * Build MCP tools definition for Responses API
3562
+ * Check if a specific model supports vision capabilities
3341
3563
  */
3342
- buildMCPToolsDefinition() {
3343
- return this.mcpServers.map((server) => {
3344
- const mcpDef = {
3345
- type: "mcp",
3346
- // Using 'mcp' as indicated by the error message
3347
- server_label: server.name,
3348
- // Use server_label as required by API
3349
- server_url: server.url
3350
- // Use server_url instead of url
3351
- };
3352
- if (server.require_approval) {
3353
- mcpDef.require_approval = server.require_approval;
3354
- }
3355
- if (server.tool_configuration?.allowed_tools) {
3356
- mcpDef.allowed_tools = server.tool_configuration.allowed_tools;
3357
- }
3358
- if (server.authorization_token) {
3359
- mcpDef.headers = {
3360
- Authorization: `Bearer ${server.authorization_token}`
3361
- };
3564
+ supportsVisionForModel(model) {
3565
+ return isKimiVisionModel(model);
3566
+ }
3567
+ getVisionSupportLevelForModel(model) {
3568
+ return this.supportsVisionForModel(model) ? "supported" : "unsupported";
3569
+ }
3570
+ resolveEndpoint(options) {
3571
+ if (options.endpoint) {
3572
+ return this.normalizeEndpoint(options.endpoint);
3573
+ }
3574
+ if (options.baseUrl) {
3575
+ const baseUrl = this.normalizeEndpoint(options.baseUrl);
3576
+ if (baseUrl.endsWith("/chat/completions")) {
3577
+ return baseUrl;
3362
3578
  }
3363
- return mcpDef;
3364
- });
3579
+ return `${baseUrl}/chat/completions`;
3580
+ }
3581
+ return ENDPOINT_KIMI_CHAT_COMPLETIONS_API;
3365
3582
  }
3366
- async handleStream(res, onPartial) {
3367
- return parseOpenAICompatibleTextStream(res, onPartial);
3583
+ normalizeEndpoint(value) {
3584
+ return value.replace(/\/+$/, "");
3368
3585
  }
3369
- async parseStream(res, onPartial) {
3370
- return parseOpenAICompatibleToolStream(res, onPartial, {
3371
- appendTextBlock: StreamTextAccumulator.addTextBlock
3586
+ };
3587
+
3588
+ // src/services/providers/mistral/MistralChatService.ts
3589
+ var MistralChatService = class extends OpenAIChatService {
3590
+ constructor(apiKey, model = MODEL_MISTRAL_SMALL_LATEST, visionModel = model, tools, endpoint = ENDPOINT_MISTRAL_CHAT_COMPLETIONS_API, responseLength, reasoningEffort) {
3591
+ super(
3592
+ apiKey,
3593
+ model,
3594
+ visionModel,
3595
+ tools,
3596
+ endpoint,
3597
+ [],
3598
+ responseLength,
3599
+ void 0,
3600
+ reasoningEffort,
3601
+ false,
3602
+ "mistral",
3603
+ false
3604
+ );
3605
+ }
3606
+ };
3607
+
3608
+ // src/services/providers/mistral/MistralChatServiceProvider.ts
3609
+ var MistralChatServiceProvider = class {
3610
+ createChatService(options) {
3611
+ this.validateRequiredOptions(options);
3612
+ const model = options.model || this.getDefaultModel();
3613
+ const visionModel = resolveVisionModel({
3614
+ model,
3615
+ visionModel: options.visionModel,
3616
+ defaultModel: this.getDefaultModel(),
3617
+ defaultVisionModel: this.getDefaultVisionModel(),
3618
+ supportsVisionForModel: (targetModel) => this.supportsVisionForModel(targetModel),
3619
+ validate: "explicit"
3372
3620
  });
3621
+ const tools = options.tools;
3622
+ const reasoningEffort = isMistralReasoningEffortModel(model) ? options.reasoning_effort : void 0;
3623
+ return new MistralChatService(
3624
+ options.apiKey,
3625
+ model,
3626
+ visionModel,
3627
+ tools,
3628
+ this.resolveEndpoint(options),
3629
+ options.responseLength,
3630
+ reasoningEffort
3631
+ );
3373
3632
  }
3374
- parseOneShot(data) {
3375
- return parseOpenAICompatibleOneShot(data);
3633
+ getProviderName() {
3634
+ return "mistral";
3635
+ }
3636
+ getSupportedModels() {
3637
+ return [...MISTRAL_SUPPORTED_MODELS];
3638
+ }
3639
+ getDefaultModel() {
3640
+ return MODEL_MISTRAL_SMALL_LATEST;
3641
+ }
3642
+ getDefaultVisionModel() {
3643
+ return MODEL_MISTRAL_SMALL_LATEST;
3644
+ }
3645
+ supportsVision() {
3646
+ return this.getVisionSupportLevel() !== "unsupported";
3647
+ }
3648
+ getVisionSupportLevel() {
3649
+ return "supported";
3650
+ }
3651
+ supportsVisionForModel(model) {
3652
+ return isMistralVisionModel(model);
3653
+ }
3654
+ getVisionSupportLevelForModel(model) {
3655
+ return this.supportsVisionForModel(model) ? "supported" : "unsupported";
3656
+ }
3657
+ validateRequiredOptions(options) {
3658
+ if (!options.apiKey?.trim()) {
3659
+ throw new Error("mistral provider requires apiKey.");
3660
+ }
3661
+ }
3662
+ resolveEndpoint(options) {
3663
+ if (options.endpoint) {
3664
+ return this.normalizeUrl(options.endpoint);
3665
+ }
3666
+ if (options.baseUrl) {
3667
+ const baseUrl = this.normalizeUrl(options.baseUrl);
3668
+ if (baseUrl.endsWith("/chat/completions")) {
3669
+ return baseUrl;
3670
+ }
3671
+ return `${baseUrl}/chat/completions`;
3672
+ }
3673
+ return ENDPOINT_MISTRAL_CHAT_COMPLETIONS_API;
3674
+ }
3675
+ normalizeUrl(value) {
3676
+ return value.trim().replace(/\/+$/, "");
3376
3677
  }
3377
3678
  };
3378
3679
 
@@ -4449,7 +4750,9 @@ If it's in another language, summarize in that language.
4449
4750
  new OpenRouterChatServiceProvider(),
4450
4751
  new ZAIChatServiceProvider(),
4451
4752
  new XAIChatServiceProvider(),
4452
- new KimiChatServiceProvider()
4753
+ new KimiChatServiceProvider(),
4754
+ new DeepSeekChatServiceProvider(),
4755
+ new MistralChatServiceProvider()
4453
4756
  ];
4454
4757
 
4455
4758
  // src/services/ChatServiceFactory.ts