@aituber-onair/chat 0.10.0 → 0.11.1

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 (167) hide show
  1. package/README.ja.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/cjs/constants/claude.d.ts +1 -0
  4. package/dist/cjs/constants/claude.d.ts.map +1 -1
  5. package/dist/cjs/constants/claude.js +3 -1
  6. package/dist/cjs/constants/claude.js.map +1 -1
  7. package/dist/cjs/index.d.ts +1 -1
  8. package/dist/cjs/index.d.ts.map +1 -1
  9. package/dist/cjs/index.js.map +1 -1
  10. package/dist/cjs/services/ChatServiceFactory.d.ts +2 -2
  11. package/dist/cjs/services/ChatServiceFactory.d.ts.map +1 -1
  12. package/dist/cjs/services/ChatServiceFactory.js +2 -24
  13. package/dist/cjs/services/ChatServiceFactory.js.map +1 -1
  14. package/dist/cjs/services/providers/ChatServiceProvider.d.ts +35 -5
  15. package/dist/cjs/services/providers/ChatServiceProvider.d.ts.map +1 -1
  16. package/dist/cjs/services/providers/claude/ClaudeChatService.d.ts.map +1 -1
  17. package/dist/cjs/services/providers/claude/ClaudeChatService.js +21 -36
  18. package/dist/cjs/services/providers/claude/ClaudeChatService.js.map +1 -1
  19. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.d.ts +3 -3
  20. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.d.ts.map +1 -1
  21. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.js +10 -4
  22. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.js.map +1 -1
  23. package/dist/cjs/services/providers/gemini/GeminiChatService.d.ts.map +1 -1
  24. package/dist/cjs/services/providers/gemini/GeminiChatService.js +28 -45
  25. package/dist/cjs/services/providers/gemini/GeminiChatService.js.map +1 -1
  26. package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.d.ts +3 -3
  27. package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.d.ts.map +1 -1
  28. package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.js +9 -4
  29. package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.js.map +1 -1
  30. package/dist/cjs/services/providers/index.d.ts +8 -0
  31. package/dist/cjs/services/providers/index.d.ts.map +1 -0
  32. package/dist/cjs/services/providers/index.js +18 -0
  33. package/dist/cjs/services/providers/index.js.map +1 -0
  34. package/dist/cjs/services/providers/kimi/KimiChatService.d.ts.map +1 -1
  35. package/dist/cjs/services/providers/kimi/KimiChatService.js +31 -162
  36. package/dist/cjs/services/providers/kimi/KimiChatService.js.map +1 -1
  37. package/dist/cjs/services/providers/kimi/KimiChatServiceProvider.d.ts +3 -3
  38. package/dist/cjs/services/providers/kimi/KimiChatServiceProvider.d.ts.map +1 -1
  39. package/dist/cjs/services/providers/kimi/KimiChatServiceProvider.js +9 -8
  40. package/dist/cjs/services/providers/kimi/KimiChatServiceProvider.js.map +1 -1
  41. package/dist/cjs/services/providers/openai/OpenAIChatService.d.ts.map +1 -1
  42. package/dist/cjs/services/providers/openai/OpenAIChatService.js +50 -199
  43. package/dist/cjs/services/providers/openai/OpenAIChatService.js.map +1 -1
  44. package/dist/cjs/services/providers/openai/OpenAIChatServiceProvider.d.ts +3 -3
  45. package/dist/cjs/services/providers/openai/OpenAIChatServiceProvider.d.ts.map +1 -1
  46. package/dist/cjs/services/providers/openai/OpenAIChatServiceProvider.js +9 -4
  47. package/dist/cjs/services/providers/openai/OpenAIChatServiceProvider.js.map +1 -1
  48. package/dist/cjs/services/providers/openrouter/OpenRouterChatService.d.ts.map +1 -1
  49. package/dist/cjs/services/providers/openrouter/OpenRouterChatService.js +31 -165
  50. package/dist/cjs/services/providers/openrouter/OpenRouterChatService.js.map +1 -1
  51. package/dist/cjs/services/providers/openrouter/OpenRouterChatServiceProvider.d.ts +3 -3
  52. package/dist/cjs/services/providers/openrouter/OpenRouterChatServiceProvider.d.ts.map +1 -1
  53. package/dist/cjs/services/providers/openrouter/OpenRouterChatServiceProvider.js +9 -6
  54. package/dist/cjs/services/providers/openrouter/OpenRouterChatServiceProvider.js.map +1 -1
  55. package/dist/cjs/services/providers/zai/ZAIChatService.d.ts.map +1 -1
  56. package/dist/cjs/services/providers/zai/ZAIChatService.js +31 -162
  57. package/dist/cjs/services/providers/zai/ZAIChatService.js.map +1 -1
  58. package/dist/cjs/services/providers/zai/ZAIChatServiceProvider.d.ts +3 -3
  59. package/dist/cjs/services/providers/zai/ZAIChatServiceProvider.d.ts.map +1 -1
  60. package/dist/cjs/services/providers/zai/ZAIChatServiceProvider.js +9 -8
  61. package/dist/cjs/services/providers/zai/ZAIChatServiceProvider.js.map +1 -1
  62. package/dist/cjs/types/mcp.d.ts +1 -0
  63. package/dist/cjs/types/mcp.d.ts.map +1 -1
  64. package/dist/cjs/utils/index.d.ts +4 -0
  65. package/dist/cjs/utils/index.d.ts.map +1 -1
  66. package/dist/cjs/utils/index.js +4 -0
  67. package/dist/cjs/utils/index.js.map +1 -1
  68. package/dist/cjs/utils/openaiCompatibleSse.d.ts +10 -0
  69. package/dist/cjs/utils/openaiCompatibleSse.d.ts.map +1 -0
  70. package/dist/cjs/utils/openaiCompatibleSse.js +124 -0
  71. package/dist/cjs/utils/openaiCompatibleSse.js.map +1 -0
  72. package/dist/cjs/utils/openaiCompatibleTools.d.ts +4 -0
  73. package/dist/cjs/utils/openaiCompatibleTools.d.ts.map +1 -0
  74. package/dist/cjs/utils/openaiCompatibleTools.js +25 -0
  75. package/dist/cjs/utils/openaiCompatibleTools.js.map +1 -0
  76. package/dist/cjs/utils/processChatFlow.d.ts +12 -0
  77. package/dist/cjs/utils/processChatFlow.d.ts.map +1 -0
  78. package/dist/cjs/utils/processChatFlow.js +22 -0
  79. package/dist/cjs/utils/processChatFlow.js.map +1 -0
  80. package/dist/cjs/utils/visionModelResolver.d.ts +12 -0
  81. package/dist/cjs/utils/visionModelResolver.d.ts.map +1 -0
  82. package/dist/cjs/utils/visionModelResolver.js +22 -0
  83. package/dist/cjs/utils/visionModelResolver.js.map +1 -0
  84. package/dist/esm/constants/claude.d.ts +1 -0
  85. package/dist/esm/constants/claude.d.ts.map +1 -1
  86. package/dist/esm/constants/claude.js +2 -0
  87. package/dist/esm/constants/claude.js.map +1 -1
  88. package/dist/esm/index.d.ts +1 -1
  89. package/dist/esm/index.d.ts.map +1 -1
  90. package/dist/esm/index.js.map +1 -1
  91. package/dist/esm/services/ChatServiceFactory.d.ts +2 -2
  92. package/dist/esm/services/ChatServiceFactory.d.ts.map +1 -1
  93. package/dist/esm/services/ChatServiceFactory.js +2 -24
  94. package/dist/esm/services/ChatServiceFactory.js.map +1 -1
  95. package/dist/esm/services/providers/ChatServiceProvider.d.ts +35 -5
  96. package/dist/esm/services/providers/ChatServiceProvider.d.ts.map +1 -1
  97. package/dist/esm/services/providers/claude/ClaudeChatService.d.ts.map +1 -1
  98. package/dist/esm/services/providers/claude/ClaudeChatService.js +21 -36
  99. package/dist/esm/services/providers/claude/ClaudeChatService.js.map +1 -1
  100. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.d.ts +3 -3
  101. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.d.ts.map +1 -1
  102. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.js +11 -5
  103. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.js.map +1 -1
  104. package/dist/esm/services/providers/gemini/GeminiChatService.d.ts.map +1 -1
  105. package/dist/esm/services/providers/gemini/GeminiChatService.js +28 -45
  106. package/dist/esm/services/providers/gemini/GeminiChatService.js.map +1 -1
  107. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.d.ts +3 -3
  108. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.d.ts.map +1 -1
  109. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.js +9 -4
  110. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.js.map +1 -1
  111. package/dist/esm/services/providers/index.d.ts +8 -0
  112. package/dist/esm/services/providers/index.d.ts.map +1 -0
  113. package/dist/esm/services/providers/index.js +15 -0
  114. package/dist/esm/services/providers/index.js.map +1 -0
  115. package/dist/esm/services/providers/kimi/KimiChatService.d.ts.map +1 -1
  116. package/dist/esm/services/providers/kimi/KimiChatService.js +31 -162
  117. package/dist/esm/services/providers/kimi/KimiChatService.js.map +1 -1
  118. package/dist/esm/services/providers/kimi/KimiChatServiceProvider.d.ts +3 -3
  119. package/dist/esm/services/providers/kimi/KimiChatServiceProvider.d.ts.map +1 -1
  120. package/dist/esm/services/providers/kimi/KimiChatServiceProvider.js +9 -8
  121. package/dist/esm/services/providers/kimi/KimiChatServiceProvider.js.map +1 -1
  122. package/dist/esm/services/providers/openai/OpenAIChatService.d.ts.map +1 -1
  123. package/dist/esm/services/providers/openai/OpenAIChatService.js +50 -199
  124. package/dist/esm/services/providers/openai/OpenAIChatService.js.map +1 -1
  125. package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.d.ts +3 -3
  126. package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.d.ts.map +1 -1
  127. package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.js +9 -4
  128. package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.js.map +1 -1
  129. package/dist/esm/services/providers/openrouter/OpenRouterChatService.d.ts.map +1 -1
  130. package/dist/esm/services/providers/openrouter/OpenRouterChatService.js +31 -165
  131. package/dist/esm/services/providers/openrouter/OpenRouterChatService.js.map +1 -1
  132. package/dist/esm/services/providers/openrouter/OpenRouterChatServiceProvider.d.ts +3 -3
  133. package/dist/esm/services/providers/openrouter/OpenRouterChatServiceProvider.d.ts.map +1 -1
  134. package/dist/esm/services/providers/openrouter/OpenRouterChatServiceProvider.js +9 -6
  135. package/dist/esm/services/providers/openrouter/OpenRouterChatServiceProvider.js.map +1 -1
  136. package/dist/esm/services/providers/zai/ZAIChatService.d.ts.map +1 -1
  137. package/dist/esm/services/providers/zai/ZAIChatService.js +31 -162
  138. package/dist/esm/services/providers/zai/ZAIChatService.js.map +1 -1
  139. package/dist/esm/services/providers/zai/ZAIChatServiceProvider.d.ts +3 -3
  140. package/dist/esm/services/providers/zai/ZAIChatServiceProvider.d.ts.map +1 -1
  141. package/dist/esm/services/providers/zai/ZAIChatServiceProvider.js +9 -8
  142. package/dist/esm/services/providers/zai/ZAIChatServiceProvider.js.map +1 -1
  143. package/dist/esm/types/mcp.d.ts +1 -0
  144. package/dist/esm/types/mcp.d.ts.map +1 -1
  145. package/dist/esm/utils/index.d.ts +4 -0
  146. package/dist/esm/utils/index.d.ts.map +1 -1
  147. package/dist/esm/utils/index.js +4 -0
  148. package/dist/esm/utils/index.js.map +1 -1
  149. package/dist/esm/utils/openaiCompatibleSse.d.ts +10 -0
  150. package/dist/esm/utils/openaiCompatibleSse.d.ts.map +1 -0
  151. package/dist/esm/utils/openaiCompatibleSse.js +119 -0
  152. package/dist/esm/utils/openaiCompatibleSse.js.map +1 -0
  153. package/dist/esm/utils/openaiCompatibleTools.d.ts +4 -0
  154. package/dist/esm/utils/openaiCompatibleTools.d.ts.map +1 -0
  155. package/dist/esm/utils/openaiCompatibleTools.js +21 -0
  156. package/dist/esm/utils/openaiCompatibleTools.js.map +1 -0
  157. package/dist/esm/utils/processChatFlow.d.ts +12 -0
  158. package/dist/esm/utils/processChatFlow.d.ts.map +1 -0
  159. package/dist/esm/utils/processChatFlow.js +19 -0
  160. package/dist/esm/utils/processChatFlow.js.map +1 -0
  161. package/dist/esm/utils/visionModelResolver.d.ts +12 -0
  162. package/dist/esm/utils/visionModelResolver.d.ts.map +1 -0
  163. package/dist/esm/utils/visionModelResolver.js +18 -0
  164. package/dist/esm/utils/visionModelResolver.js.map +1 -0
  165. package/dist/umd/aituber-onair-chat.js +1592 -1875
  166. package/dist/umd/aituber-onair-chat.min.js +6 -15
  167. package/package.json +1 -1
@@ -62,6 +62,7 @@ var AITuberOnAirChat = (() => {
62
62
  MODEL_CLAUDE_4_5_HAIKU: () => MODEL_CLAUDE_4_5_HAIKU,
63
63
  MODEL_CLAUDE_4_5_OPUS: () => MODEL_CLAUDE_4_5_OPUS,
64
64
  MODEL_CLAUDE_4_5_SONNET: () => MODEL_CLAUDE_4_5_SONNET,
65
+ MODEL_CLAUDE_4_6_OPUS: () => MODEL_CLAUDE_4_6_OPUS,
65
66
  MODEL_CLAUDE_4_OPUS: () => MODEL_CLAUDE_4_OPUS,
66
67
  MODEL_CLAUDE_4_SONNET: () => MODEL_CLAUDE_4_SONNET,
67
68
  MODEL_GEMINI_2_0_FLASH: () => MODEL_GEMINI_2_0_FLASH,
@@ -123,6 +124,7 @@ var AITuberOnAirChat = (() => {
123
124
  ZAI_VISION_SUPPORTED_MODELS: () => ZAI_VISION_SUPPORTED_MODELS,
124
125
  allowsReasoningMinimal: () => allowsReasoningMinimal,
125
126
  allowsReasoningNone: () => allowsReasoningNone,
127
+ buildOpenAICompatibleTools: () => buildOpenAICompatibleTools,
126
128
  getMaxTokensForResponseLength: () => getMaxTokensForResponseLength,
127
129
  installGASFetch: () => installGASFetch,
128
130
  isGPT5Model: () => isGPT5Model,
@@ -131,6 +133,11 @@ var AITuberOnAirChat = (() => {
131
133
  isOpenRouterVisionModel: () => isOpenRouterVisionModel,
132
134
  isZaiToolStreamModel: () => isZaiToolStreamModel,
133
135
  isZaiVisionModel: () => isZaiVisionModel,
136
+ parseOpenAICompatibleOneShot: () => parseOpenAICompatibleOneShot,
137
+ parseOpenAICompatibleTextStream: () => parseOpenAICompatibleTextStream,
138
+ parseOpenAICompatibleToolStream: () => parseOpenAICompatibleToolStream,
139
+ processChatWithOptionalTools: () => processChatWithOptionalTools,
140
+ resolveVisionModel: () => resolveVisionModel,
134
141
  runOnceText: () => runOnceText,
135
142
  screenplayToText: () => screenplayToText,
136
143
  textToScreenplay: () => textToScreenplay,
@@ -211,6 +218,7 @@ var AITuberOnAirChat = (() => {
211
218
  var MODEL_CLAUDE_4_5_SONNET = "claude-sonnet-4-5-20250929";
212
219
  var MODEL_CLAUDE_4_5_HAIKU = "claude-haiku-4-5-20251001";
213
220
  var MODEL_CLAUDE_4_5_OPUS = "claude-opus-4-5-20251101";
221
+ var MODEL_CLAUDE_4_6_OPUS = "claude-opus-4-6";
214
222
  var CLAUDE_VISION_SUPPORTED_MODELS = [
215
223
  MODEL_CLAUDE_3_HAIKU,
216
224
  MODEL_CLAUDE_3_5_HAIKU,
@@ -220,7 +228,8 @@ var AITuberOnAirChat = (() => {
220
228
  MODEL_CLAUDE_4_OPUS,
221
229
  MODEL_CLAUDE_4_5_SONNET,
222
230
  MODEL_CLAUDE_4_5_HAIKU,
223
- MODEL_CLAUDE_4_5_OPUS
231
+ MODEL_CLAUDE_4_5_OPUS,
232
+ MODEL_CLAUDE_4_6_OPUS
224
233
  ];
225
234
 
226
235
  // src/constants/openrouter.ts
@@ -365,43 +374,6 @@ If it's in English, summarize in English.
365
374
  If it's in another language, summarize in that language.
366
375
  `;
367
376
 
368
- // src/utils/streamTextAccumulator.ts
369
- var StreamTextAccumulator = class {
370
- /**
371
- * Append text to the blocks array, merging with the last block if it's a text block
372
- * @param blocks Array of chat blocks
373
- * @param text Text to append
374
- */
375
- static append(blocks, text) {
376
- if (!text) return;
377
- const lastBlock = blocks[blocks.length - 1];
378
- if (lastBlock && lastBlock.type === "text") {
379
- lastBlock.text += text;
380
- } else {
381
- blocks.push({ type: "text", text });
382
- }
383
- }
384
- /**
385
- * Get the full concatenated text from all text blocks
386
- * @param blocks Array of chat blocks
387
- * @returns Concatenated text from all text blocks
388
- */
389
- static getFullText(blocks) {
390
- return blocks.filter(
391
- (block) => block.type === "text"
392
- ).map((block) => block.text).join("");
393
- }
394
- /**
395
- * Add a text block without merging
396
- * @param blocks Array of chat blocks
397
- * @param text Text to add as a new block
398
- */
399
- static addTextBlock(blocks, text) {
400
- if (!text) return;
401
- blocks.push({ type: "text", text });
402
- }
403
- };
404
-
405
377
  // src/utils/chatServiceHttpClient.ts
406
378
  var HttpError = class extends Error {
407
379
  constructor(status, statusText, body) {
@@ -528,32 +500,311 @@ If it's in another language, summarize in that language.
528
500
  _ChatServiceHttpClient.fetchImpl = (u, i) => fetch(u, i);
529
501
  var ChatServiceHttpClient = _ChatServiceHttpClient;
530
502
 
531
- // src/services/providers/openai/OpenAIChatService.ts
532
- var OpenAIChatService = class {
503
+ // src/utils/streamTextAccumulator.ts
504
+ var StreamTextAccumulator = class {
505
+ /**
506
+ * Append text to the blocks array, merging with the last block if it's a text block
507
+ * @param blocks Array of chat blocks
508
+ * @param text Text to append
509
+ */
510
+ static append(blocks, text) {
511
+ if (!text) return;
512
+ const lastBlock = blocks[blocks.length - 1];
513
+ if (lastBlock && lastBlock.type === "text") {
514
+ lastBlock.text += text;
515
+ } else {
516
+ blocks.push({ type: "text", text });
517
+ }
518
+ }
519
+ /**
520
+ * Get the full concatenated text from all text blocks
521
+ * @param blocks Array of chat blocks
522
+ * @returns Concatenated text from all text blocks
523
+ */
524
+ static getFullText(blocks) {
525
+ return blocks.filter(
526
+ (block) => block.type === "text"
527
+ ).map((block) => block.text).join("");
528
+ }
529
+ /**
530
+ * Add a text block without merging
531
+ * @param blocks Array of chat blocks
532
+ * @param text Text to add as a new block
533
+ */
534
+ static addTextBlock(blocks, text) {
535
+ if (!text) return;
536
+ blocks.push({ type: "text", text });
537
+ }
538
+ };
539
+
540
+ // src/utils/emotionParser.ts
541
+ var emotions = ["happy", "sad", "angry", "surprised", "neutral"];
542
+ var EMOTION_TAG_REGEX = /\[([a-z]+)\]/i;
543
+ var EMOTION_TAG_CLEANUP_REGEX = /\[[a-z]+\]\s*/gi;
544
+ var EmotionParser = class {
545
+ /**
546
+ * Extract emotion from text and return clean text
547
+ * @param text Text that may contain emotion tags like [happy]
548
+ * @returns Object containing extracted emotion and clean text
549
+ */
550
+ static extractEmotion(text) {
551
+ const match = text.match(EMOTION_TAG_REGEX);
552
+ if (match) {
553
+ const emotion = match[1].toLowerCase();
554
+ const cleanText = text.replace(EMOTION_TAG_CLEANUP_REGEX, "").trim();
555
+ return {
556
+ emotion,
557
+ cleanText
558
+ };
559
+ }
560
+ return { cleanText: text };
561
+ }
562
+ /**
563
+ * Check if an emotion is valid
564
+ * @param emotion Emotion string to validate
565
+ * @returns True if the emotion is valid
566
+ */
567
+ static isValidEmotion(emotion) {
568
+ return emotions.includes(emotion);
569
+ }
570
+ /**
571
+ * Remove all emotion tags from text
572
+ * @param text Text containing emotion tags
573
+ * @returns Clean text without emotion tags
574
+ */
575
+ static cleanEmotionTags(text) {
576
+ return text.replace(EMOTION_TAG_CLEANUP_REGEX, "").trim();
577
+ }
578
+ /**
579
+ * Add emotion tag to text
580
+ * @param emotion Emotion to add
581
+ * @param text Text content
582
+ * @returns Text with emotion tag prepended
583
+ */
584
+ static addEmotionTag(emotion, text) {
585
+ return `[${emotion}] ${text}`;
586
+ }
587
+ };
588
+
589
+ // src/utils/screenplay.ts
590
+ function textToScreenplay(text) {
591
+ const { emotion, cleanText } = EmotionParser.extractEmotion(text);
592
+ if (emotion) {
593
+ return {
594
+ emotion,
595
+ text: cleanText
596
+ };
597
+ }
598
+ return { text: cleanText };
599
+ }
600
+ function textsToScreenplay(texts) {
601
+ return texts.map((text) => textToScreenplay(text));
602
+ }
603
+ function screenplayToText(screenplay) {
604
+ if (screenplay.emotion) {
605
+ return EmotionParser.addEmotionTag(screenplay.emotion, screenplay.text);
606
+ }
607
+ return screenplay.text;
608
+ }
609
+
610
+ // src/utils/runOnce.ts
611
+ async function runOnceText(chat, messages) {
612
+ const { blocks } = await chat.chatOnce(messages, false, () => {
613
+ });
614
+ return StreamTextAccumulator.getFullText(blocks);
615
+ }
616
+
617
+ // src/utils/openaiCompatibleSse.ts
618
+ var parseJsonPayload = (payload, onJsonError) => {
619
+ try {
620
+ return JSON.parse(payload);
621
+ } catch (error) {
622
+ if (onJsonError) {
623
+ onJsonError(payload, error);
624
+ return void 0;
625
+ }
626
+ throw error;
627
+ }
628
+ };
629
+ var forEachSsePayload = async (res, onPayload) => {
630
+ const reader = res.body?.getReader();
631
+ if (!reader) {
632
+ throw new Error("Response body is null.");
633
+ }
634
+ const dec = new TextDecoder();
635
+ let buf = "";
636
+ let shouldStop = false;
637
+ while (!shouldStop) {
638
+ const { done, value } = await reader.read();
639
+ if (done) break;
640
+ buf += dec.decode(value, { stream: true });
641
+ const lines = buf.split("\n");
642
+ buf = lines.pop() || "";
643
+ for (const line of lines) {
644
+ const trimmedLine = line.trim();
645
+ if (!trimmedLine || trimmedLine.startsWith(":")) continue;
646
+ if (!trimmedLine.startsWith("data:")) continue;
647
+ const payload = trimmedLine.slice(5).trim();
648
+ if (payload === "[DONE]") {
649
+ shouldStop = true;
650
+ break;
651
+ }
652
+ onPayload(payload);
653
+ }
654
+ }
655
+ };
656
+ async function parseOpenAICompatibleTextStream(res, onPartial, options = {}) {
657
+ let full = "";
658
+ await forEachSsePayload(res, (payload) => {
659
+ const json = parseJsonPayload(payload, options.onJsonError);
660
+ if (!json) return;
661
+ const content = json.choices?.[0]?.delta?.content || "";
662
+ if (content) {
663
+ onPartial(content);
664
+ full += content;
665
+ }
666
+ });
667
+ return full;
668
+ }
669
+ async function parseOpenAICompatibleToolStream(res, onPartial, options = {}) {
670
+ const textBlocks = [];
671
+ const toolCallsMap = /* @__PURE__ */ new Map();
672
+ const appendTextBlock = options.appendTextBlock ?? StreamTextAccumulator.append;
673
+ await forEachSsePayload(res, (payload) => {
674
+ const json = parseJsonPayload(payload, options.onJsonError);
675
+ if (!json) return;
676
+ const delta = json.choices?.[0]?.delta;
677
+ if (delta?.content) {
678
+ onPartial(delta.content);
679
+ appendTextBlock(textBlocks, delta.content);
680
+ }
681
+ if (delta?.tool_calls) {
682
+ delta.tool_calls.forEach((c) => {
683
+ const entry = toolCallsMap.get(c.index) ?? {
684
+ id: c.id,
685
+ name: c.function?.name,
686
+ args: ""
687
+ };
688
+ entry.args += c.function?.arguments || "";
689
+ toolCallsMap.set(c.index, entry);
690
+ });
691
+ }
692
+ });
693
+ const toolBlocks = Array.from(toolCallsMap.entries()).sort((a, b) => a[0] - b[0]).map(([_, e]) => ({
694
+ type: "tool_use",
695
+ id: e.id,
696
+ name: e.name,
697
+ input: JSON.parse(e.args || "{}")
698
+ }));
699
+ const blocks = [...textBlocks, ...toolBlocks];
700
+ return {
701
+ blocks,
702
+ stop_reason: toolBlocks.length ? "tool_use" : "end"
703
+ };
704
+ }
705
+ function parseOpenAICompatibleOneShot(data) {
706
+ const choice = data?.choices?.[0];
707
+ const blocks = [];
708
+ if (choice?.message?.tool_calls?.length) {
709
+ choice.message.tool_calls.forEach(
710
+ (c) => blocks.push({
711
+ type: "tool_use",
712
+ id: c.id,
713
+ name: c.function?.name,
714
+ input: JSON.parse(c.function?.arguments || "{}")
715
+ })
716
+ );
717
+ } else if (choice?.message?.content) {
718
+ blocks.push({ type: "text", text: choice.message.content });
719
+ }
720
+ return {
721
+ blocks,
722
+ stop_reason: choice?.finish_reason === "tool_calls" || blocks.some((b) => b.type === "tool_use") ? "tool_use" : "end"
723
+ };
724
+ }
725
+
726
+ // src/utils/openaiCompatibleTools.ts
727
+ var buildOpenAICompatibleTools = (tools, format = "chat-completions") => {
728
+ if (tools.length === 0) return [];
729
+ if (format === "responses") {
730
+ return tools.map((t) => ({
731
+ type: "function",
732
+ name: t.name,
733
+ description: t.description,
734
+ parameters: t.parameters
735
+ }));
736
+ }
737
+ return tools.map((t) => ({
738
+ type: "function",
739
+ function: {
740
+ name: t.name,
741
+ description: t.description,
742
+ parameters: t.parameters
743
+ }
744
+ }));
745
+ };
746
+
747
+ // src/utils/processChatFlow.ts
748
+ async function processChatWithOptionalTools(options) {
749
+ if (!options.hasTools) {
750
+ const full = await options.runWithoutTools();
751
+ await options.onCompleteResponse(full);
752
+ return;
753
+ }
754
+ const result = await options.runWithTools();
755
+ if (options.onToolBlocks) {
756
+ options.onToolBlocks(result.blocks);
757
+ }
758
+ if (result.stop_reason === "end") {
759
+ const full = StreamTextAccumulator.getFullText(
760
+ result.blocks
761
+ );
762
+ await options.onCompleteResponse(full);
763
+ return;
764
+ }
765
+ throw new Error(options.toolErrorMessage);
766
+ }
767
+
768
+ // src/utils/visionModelResolver.ts
769
+ var resolveVisionModel = (options) => {
770
+ const baseModel = options.model ?? options.defaultModel;
771
+ const resolved = options.visionModel ?? (options.supportsVisionForModel(baseModel) ? baseModel : options.defaultVisionModel);
772
+ if (options.validate === "explicit" && options.visionModel && !options.supportsVisionForModel(options.visionModel)) {
773
+ throw new Error(
774
+ `Model ${options.visionModel} does not support vision capabilities.`
775
+ );
776
+ }
777
+ if (options.validate === "resolved" && !options.supportsVisionForModel(resolved)) {
778
+ throw new Error(`Model ${resolved} does not support vision capabilities.`);
779
+ }
780
+ return resolved;
781
+ };
782
+
783
+ // src/services/providers/claude/ClaudeChatService.ts
784
+ var ClaudeChatService = class {
533
785
  /**
534
786
  * Constructor
535
- * @param apiKey OpenAI API key
787
+ * @param apiKey Anthropic API key
536
788
  * @param model Name of the model to use
537
789
  * @param visionModel Name of the vision model
790
+ * @param tools Array of tool definitions
791
+ * @param mcpServers Array of MCP server configurations (optional)
792
+ * @throws Error if the vision model doesn't support vision capabilities
538
793
  */
539
- 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) {
794
+ constructor(apiKey, model = MODEL_CLAUDE_3_HAIKU, visionModel = MODEL_CLAUDE_3_HAIKU, tools = [], mcpServers = [], responseLength) {
540
795
  /** Provider name */
541
- this.provider = "openai";
796
+ this.provider = "claude";
542
797
  this.apiKey = apiKey;
543
- this.model = model;
544
- this.tools = tools || [];
545
- this.endpoint = endpoint;
798
+ this.model = model || MODEL_CLAUDE_3_HAIKU;
799
+ this.visionModel = visionModel || MODEL_CLAUDE_3_HAIKU;
800
+ this.tools = tools;
546
801
  this.mcpServers = mcpServers;
547
802
  this.responseLength = responseLength;
548
- this.verbosity = verbosity;
549
- this.reasoning_effort = reasoning_effort;
550
- this.enableReasoningSummary = enableReasoningSummary;
551
- if (!VISION_SUPPORTED_MODELS.includes(visionModel)) {
803
+ if (!CLAUDE_VISION_SUPPORTED_MODELS.includes(this.visionModel)) {
552
804
  throw new Error(
553
- `Model ${visionModel} does not support vision capabilities.`
805
+ `Model ${this.visionModel} does not support vision capabilities.`
554
806
  );
555
807
  }
556
- this.visionModel = visionModel;
557
808
  }
558
809
  /**
559
810
  * Get the current model name
@@ -569,6 +820,36 @@ If it's in another language, summarize in that language.
569
820
  getVisionModel() {
570
821
  return this.visionModel;
571
822
  }
823
+ /**
824
+ * Get configured MCP servers
825
+ * @returns Array of MCP server configurations
826
+ */
827
+ getMCPServers() {
828
+ return this.mcpServers;
829
+ }
830
+ /**
831
+ * Add MCP server configuration
832
+ * @param serverConfig MCP server configuration
833
+ */
834
+ addMCPServer(serverConfig) {
835
+ this.mcpServers.push(serverConfig);
836
+ }
837
+ /**
838
+ * Remove MCP server by name
839
+ * @param serverName Name of the server to remove
840
+ */
841
+ removeMCPServer(serverName) {
842
+ this.mcpServers = this.mcpServers.filter(
843
+ (server) => server.name !== serverName
844
+ );
845
+ }
846
+ /**
847
+ * Check if MCP servers are configured
848
+ * @returns True if MCP servers are configured
849
+ */
850
+ hasMCPServers() {
851
+ return this.mcpServers.length > 0;
852
+ }
572
853
  /**
573
854
  * Process chat messages
574
855
  * @param messages Array of messages to send
@@ -576,582 +857,410 @@ If it's in another language, summarize in that language.
576
857
  * @param onCompleteResponse Callback to execute when response is complete
577
858
  */
578
859
  async processChat(messages, onPartialResponse, onCompleteResponse) {
579
- if (this.tools.length === 0) {
580
- const res = await this.callOpenAI(messages, this.model, true);
581
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
582
- try {
583
- if (isResponsesAPI) {
584
- const result = await this.parseResponsesStream(
585
- res,
586
- onPartialResponse
587
- );
588
- const full = result.blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
589
- await onCompleteResponse(full);
590
- } else {
591
- const full = await this.handleStream(res, onPartialResponse);
592
- await onCompleteResponse(full);
593
- }
594
- } catch (error) {
595
- console.error("[processChat] Error in streaming/completion:", error);
596
- throw error;
597
- }
598
- return;
599
- }
600
- const { blocks, stop_reason } = await this.chatOnce(messages);
601
- if (stop_reason === "end") {
602
- const full = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
603
- await onCompleteResponse(full);
604
- return;
605
- }
606
- throw new Error(
607
- "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
608
- );
860
+ await processChatWithOptionalTools({
861
+ hasTools: this.tools.length > 0 || this.mcpServers.length > 0,
862
+ runWithoutTools: async () => {
863
+ const res = await this.callClaude(messages, this.model, true);
864
+ return this.parsePureStream(res, onPartialResponse);
865
+ },
866
+ runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
867
+ onCompleteResponse,
868
+ toolErrorMessage: "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
869
+ });
609
870
  }
610
871
  /**
611
872
  * Process chat messages with images
612
873
  * @param messages Array of messages to send (including images)
613
874
  * @param onPartialResponse Callback to receive each part of streaming response
614
875
  * @param onCompleteResponse Callback to execute when response is complete
615
- * @throws Error if the selected model doesn't support vision
616
876
  */
617
877
  async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
618
- try {
619
- if (this.tools.length === 0) {
620
- const res = await this.callOpenAI(messages, this.visionModel, true);
621
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
622
- try {
623
- if (isResponsesAPI) {
624
- const result = await this.parseResponsesStream(
625
- res,
626
- onPartialResponse
627
- );
628
- const full = result.blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
629
- await onCompleteResponse(full);
630
- } else {
631
- const full = await this.handleStream(res, onPartialResponse);
632
- await onCompleteResponse(full);
633
- }
634
- } catch (streamError) {
635
- console.error(
636
- "[processVisionChat] Error in streaming/completion:",
637
- streamError
638
- );
639
- throw streamError;
640
- }
641
- return;
642
- }
643
- const { blocks, stop_reason } = await this.visionChatOnce(
644
- messages,
645
- true,
646
- onPartialResponse
647
- );
648
- if (stop_reason === "end") {
649
- const full = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
650
- await onCompleteResponse(full);
651
- return;
652
- }
653
- throw new Error(
654
- "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
655
- );
656
- } catch (error) {
657
- console.error("Error in processVisionChat:", error);
658
- throw error;
659
- }
660
- }
661
- /**
662
- * Process chat messages with tools (text only)
663
- * @param messages Array of messages to send
664
- * @param stream Whether to use streaming
665
- * @param onPartialResponse Callback for partial responses
666
- * @param maxTokens Maximum tokens for response (optional)
667
- * @returns Tool chat completion
668
- */
669
- async chatOnce(messages, stream = true, onPartialResponse = () => {
670
- }, maxTokens) {
671
- const res = await this.callOpenAI(messages, this.model, stream, maxTokens);
672
- return this.parseResponse(res, stream, onPartialResponse);
673
- }
674
- /**
675
- * Process vision chat messages with tools
676
- * @param messages Array of messages to send (including images)
677
- * @param stream Whether to use streaming
678
- * @param onPartialResponse Callback for partial responses
679
- * @param maxTokens Maximum tokens for response (optional)
680
- * @returns Tool chat completion
681
- */
682
- async visionChatOnce(messages, stream = false, onPartialResponse = () => {
683
- }, maxTokens) {
684
- const res = await this.callOpenAI(
685
- messages,
686
- this.visionModel,
687
- stream,
688
- maxTokens
689
- );
690
- return this.parseResponse(res, stream, onPartialResponse);
691
- }
692
- /**
693
- * Parse response based on endpoint type
694
- */
695
- async parseResponse(res, stream, onPartialResponse) {
696
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
697
- if (isResponsesAPI) {
698
- return stream ? this.parseResponsesStream(res, onPartialResponse) : this.parseResponsesOneShot(await res.json());
699
- }
700
- return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
701
- }
702
- async callOpenAI(messages, model, stream = false, maxTokens) {
703
- const body = this.buildRequestBody(messages, model, stream, maxTokens);
704
- const res = await ChatServiceHttpClient.post(this.endpoint, body, {
705
- Authorization: `Bearer ${this.apiKey}`
878
+ await processChatWithOptionalTools({
879
+ hasTools: this.tools.length > 0 || this.mcpServers.length > 0,
880
+ runWithoutTools: async () => {
881
+ const res = await this.callClaude(messages, this.visionModel, true);
882
+ return this.parsePureStream(res, onPartialResponse);
883
+ },
884
+ runWithTools: () => this.visionChatOnce(messages),
885
+ onCompleteResponse,
886
+ toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
706
887
  });
707
- return res;
708
- }
709
- /**
710
- * Build request body based on the endpoint type
711
- */
712
- buildRequestBody(messages, model, stream, maxTokens) {
713
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
714
- this.validateMCPCompatibility();
715
- const body = {
716
- model,
717
- stream
718
- };
719
- const tokenLimit = maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength);
720
- if (isResponsesAPI) {
721
- body.max_output_tokens = tokenLimit;
722
- } else {
723
- body.max_completion_tokens = tokenLimit;
724
- }
725
- if (isResponsesAPI) {
726
- body.input = this.cleanMessagesForResponsesAPI(messages);
727
- } else {
728
- body.messages = messages;
729
- }
730
- if (isGPT5Model(model)) {
731
- if (isResponsesAPI) {
732
- if (this.reasoning_effort) {
733
- body.reasoning = {
734
- ...body.reasoning,
735
- effort: this.reasoning_effort
736
- };
737
- if (this.enableReasoningSummary) {
738
- body.reasoning.summary = "auto";
739
- }
740
- }
741
- if (this.verbosity) {
742
- body.text = {
743
- ...body.text,
744
- format: { type: "text" },
745
- verbosity: this.verbosity
746
- };
747
- }
748
- } else {
749
- if (this.reasoning_effort) {
750
- body.reasoning_effort = this.reasoning_effort;
751
- }
752
- if (this.verbosity) {
753
- body.verbosity = this.verbosity;
754
- }
755
- }
756
- }
757
- const tools = this.buildToolsDefinition();
758
- if (tools.length > 0) {
759
- body.tools = tools;
760
- if (!isResponsesAPI) {
761
- body.tool_choice = "auto";
762
- }
763
- }
764
- return body;
765
888
  }
766
889
  /**
767
- * Validate MCP servers compatibility with the current endpoint
890
+ * Convert AITuber OnAir messages to Claude format
891
+ * @param messages Array of messages
892
+ * @returns Claude formatted messages
768
893
  */
769
- validateMCPCompatibility() {
770
- if (this.mcpServers.length > 0 && this.endpoint === ENDPOINT_OPENAI_CHAT_COMPLETIONS_API) {
771
- throw new Error(
772
- `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.`
773
- );
774
- }
894
+ convertMessagesToClaudeFormat(messages) {
895
+ return messages.map((msg) => {
896
+ return {
897
+ role: this.mapRoleToClaude(msg.role),
898
+ content: msg.content
899
+ };
900
+ });
775
901
  }
776
902
  /**
777
- * Clean messages for Responses API (remove timestamp and other extra properties)
903
+ * Convert AITuber OnAir vision messages to Claude format
904
+ * @param messages Array of vision messages
905
+ * @returns Claude formatted vision messages
778
906
  */
779
- cleanMessagesForResponsesAPI(messages) {
907
+ convertVisionMessagesToClaudeFormat(messages) {
780
908
  return messages.map((msg) => {
781
- const role = msg.role === "tool" ? "user" : msg.role;
782
- const cleanMsg = {
783
- role
784
- };
785
909
  if (typeof msg.content === "string") {
786
- cleanMsg.content = msg.content;
787
- } else if (Array.isArray(msg.content)) {
788
- cleanMsg.content = msg.content.map((block) => {
789
- if (block.type === "text") {
790
- return {
791
- type: "input_text",
792
- text: block.text
793
- };
794
- } else if (block.type === "image_url") {
910
+ return {
911
+ role: this.mapRoleToClaude(msg.role),
912
+ content: [
913
+ {
914
+ type: "text",
915
+ text: msg.content
916
+ }
917
+ ]
918
+ };
919
+ }
920
+ if (Array.isArray(msg.content)) {
921
+ const content = msg.content.map((block) => {
922
+ if (block.type === "image_url") {
923
+ if (block.image_url.url.startsWith("data:")) {
924
+ const m = block.image_url.url.match(
925
+ /^data:([^;]+);base64,(.+)$/
926
+ );
927
+ if (m) {
928
+ return {
929
+ type: "image",
930
+ source: { type: "base64", media_type: m[1], data: m[2] }
931
+ };
932
+ }
933
+ return null;
934
+ }
795
935
  return {
796
- type: "input_image",
797
- image_url: block.image_url.url
798
- // Extract the URL string directly
936
+ type: "image",
937
+ source: {
938
+ type: "url",
939
+ url: block.image_url.url,
940
+ media_type: this.getMimeTypeFromUrl(block.image_url.url)
941
+ }
799
942
  };
800
943
  }
801
944
  return block;
802
- });
803
- } else {
804
- cleanMsg.content = msg.content;
945
+ }).filter((b) => b);
946
+ return {
947
+ role: this.mapRoleToClaude(msg.role),
948
+ content
949
+ };
805
950
  }
806
- return cleanMsg;
951
+ return {
952
+ role: this.mapRoleToClaude(msg.role),
953
+ content: []
954
+ };
807
955
  });
808
956
  }
809
957
  /**
810
- * Build tools definition based on the endpoint type
958
+ * Map AITuber OnAir roles to Claude roles
959
+ * @param role AITuber OnAir role
960
+ * @returns Claude role
811
961
  */
812
- buildToolsDefinition() {
813
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
814
- const toolDefs = [];
815
- if (this.tools.length > 0) {
816
- if (isResponsesAPI) {
817
- toolDefs.push(
818
- ...this.tools.map((t) => ({
819
- type: "function",
820
- name: t.name,
821
- description: t.description,
822
- parameters: t.parameters
823
- }))
824
- );
825
- } else {
826
- toolDefs.push(
827
- ...this.tools.map((t) => ({
828
- type: "function",
829
- function: {
830
- name: t.name,
831
- description: t.description,
832
- parameters: t.parameters
833
- }
834
- }))
835
- );
836
- }
837
- }
838
- if (this.mcpServers.length > 0 && isResponsesAPI) {
839
- toolDefs.push(...this.buildMCPToolsDefinition());
962
+ mapRoleToClaude(role) {
963
+ switch (role) {
964
+ case "system":
965
+ return "system";
966
+ case "user":
967
+ return "user";
968
+ case "assistant":
969
+ return "assistant";
970
+ default:
971
+ return "user";
840
972
  }
841
- return toolDefs;
842
973
  }
843
974
  /**
844
- * Build MCP tools definition for Responses API
975
+ * Get MIME type from URL
976
+ * @param url Image URL
977
+ * @returns MIME type
845
978
  */
846
- buildMCPToolsDefinition() {
847
- return this.mcpServers.map((server) => {
848
- const mcpDef = {
849
- type: "mcp",
850
- // Using 'mcp' as indicated by the error message
851
- server_label: server.name,
852
- // Use server_label as required by API
853
- server_url: server.url
854
- // Use server_url instead of url
855
- };
856
- if (server.tool_configuration?.allowed_tools) {
857
- mcpDef.allowed_tools = server.tool_configuration.allowed_tools;
858
- }
859
- if (server.authorization_token) {
860
- mcpDef.headers = {
861
- Authorization: `Bearer ${server.authorization_token}`
862
- };
863
- }
864
- return mcpDef;
865
- });
979
+ getMimeTypeFromUrl(url) {
980
+ const extension = url.split(".").pop()?.toLowerCase();
981
+ switch (extension) {
982
+ case "jpg":
983
+ case "jpeg":
984
+ return "image/jpeg";
985
+ case "png":
986
+ return "image/png";
987
+ case "gif":
988
+ return "image/gif";
989
+ case "webp":
990
+ return "image/webp";
991
+ default:
992
+ return "image/jpeg";
993
+ }
866
994
  }
867
- async handleStream(res, onPartial) {
868
- const reader = res.body.getReader();
869
- const dec = new TextDecoder();
870
- let buffer = "";
871
- let full = "";
872
- while (true) {
873
- const { done, value } = await reader.read();
874
- if (done) break;
875
- buffer += dec.decode(value, { stream: true });
876
- let idx;
877
- while ((idx = buffer.indexOf("\n\n")) !== -1) {
878
- const raw = buffer.slice(0, idx).trim();
879
- buffer = buffer.slice(idx + 2);
880
- if (!raw.startsWith("data:")) continue;
881
- const jsonStr = raw.slice(5).trim();
882
- if (jsonStr === "[DONE]") {
883
- buffer = "";
884
- break;
885
- }
886
- const json = JSON.parse(jsonStr);
887
- const content = json.choices[0]?.delta?.content || "";
888
- if (content) {
889
- onPartial(content);
890
- full += content;
891
- }
892
- }
995
+ /**
996
+ * Call Claude API
997
+ * @param messages Array of messages to send
998
+ * @param model Model name
999
+ * @param stream Whether to stream the response
1000
+ * @param maxTokens Maximum tokens for response (optional)
1001
+ * @returns Response
1002
+ */
1003
+ async callClaude(messages, model, stream, maxTokens) {
1004
+ const system = messages.find((m) => m.role === "system")?.content ?? "";
1005
+ const content = messages.filter((m) => m.role !== "system");
1006
+ const hasVision = content.some(
1007
+ (m) => Array.isArray(m.content) && m.content.some(
1008
+ (b) => b.type === "image_url" || b.type === "image"
1009
+ )
1010
+ );
1011
+ const body = {
1012
+ model,
1013
+ system,
1014
+ messages: hasVision ? this.convertVisionMessagesToClaudeFormat(
1015
+ content
1016
+ ) : this.convertMessagesToClaudeFormat(content),
1017
+ stream,
1018
+ max_tokens: maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength)
1019
+ };
1020
+ if (this.tools.length) {
1021
+ body.tools = this.tools.map((t) => ({
1022
+ name: t.name,
1023
+ description: t.description,
1024
+ input_schema: t.parameters
1025
+ }));
1026
+ body.tool_choice = { type: "auto" };
1027
+ }
1028
+ if (this.mcpServers.length > 0) {
1029
+ body.mcp_servers = this.mcpServers;
1030
+ }
1031
+ const headers = {
1032
+ "Content-Type": "application/json",
1033
+ "x-api-key": this.apiKey,
1034
+ "anthropic-version": "2023-06-01",
1035
+ "anthropic-dangerous-direct-browser-access": "true"
1036
+ };
1037
+ if (this.mcpServers.length > 0) {
1038
+ headers["anthropic-beta"] = "mcp-client-2025-04-04";
893
1039
  }
894
- return full;
1040
+ const res = await ChatServiceHttpClient.post(
1041
+ ENDPOINT_CLAUDE_API,
1042
+ body,
1043
+ headers
1044
+ );
1045
+ return res;
895
1046
  }
1047
+ /**
1048
+ * Parse stream response
1049
+ * @param res Response
1050
+ * @param onPartial Callback to receive each part of streaming response
1051
+ * @returns ClaudeInternalCompletion
1052
+ */
896
1053
  async parseStream(res, onPartial) {
897
1054
  const reader = res.body.getReader();
898
1055
  const dec = new TextDecoder();
899
1056
  const textBlocks = [];
900
- const toolCallsMap = /* @__PURE__ */ new Map();
1057
+ const toolCalls = /* @__PURE__ */ new Map();
901
1058
  let buf = "";
902
1059
  while (true) {
903
1060
  const { done, value } = await reader.read();
904
1061
  if (done) break;
905
1062
  buf += dec.decode(value, { stream: true });
906
- let sep;
907
- while ((sep = buf.indexOf("\n\n")) !== -1) {
908
- const raw = buf.slice(0, sep).trim();
909
- buf = buf.slice(sep + 2);
910
- if (!raw.startsWith("data:")) continue;
911
- const payload = raw.slice(5).trim();
912
- if (payload === "[DONE]") {
913
- buf = "";
914
- break;
915
- }
916
- const json = JSON.parse(payload);
917
- const delta = json.choices[0].delta;
918
- if (delta.content) {
919
- onPartial(delta.content);
920
- textBlocks.push({ type: "text", text: delta.content });
1063
+ let nl;
1064
+ while ((nl = buf.indexOf("\n")) !== -1) {
1065
+ const line = buf.slice(0, nl).trim();
1066
+ buf = buf.slice(nl + 1);
1067
+ if (!line.startsWith("data:")) continue;
1068
+ const payload = line.slice(5).trim();
1069
+ if (payload === "[DONE]") break;
1070
+ const ev = JSON.parse(payload);
1071
+ if (ev.type === "content_block_delta" && ev.delta?.text) {
1072
+ onPartial(ev.delta.text);
1073
+ textBlocks.push({ type: "text", text: ev.delta.text });
921
1074
  }
922
- if (delta.tool_calls) {
923
- delta.tool_calls.forEach((c) => {
924
- const entry = toolCallsMap.get(c.index) ?? {
925
- id: c.id,
926
- name: c.function.name,
927
- args: ""
928
- };
929
- entry.args += c.function.arguments || "";
930
- toolCallsMap.set(c.index, entry);
1075
+ if (ev.type === "content_block_start" && ev.content_block?.type === "tool_use") {
1076
+ toolCalls.set(ev.index, {
1077
+ id: ev.content_block.id,
1078
+ name: ev.content_block.name,
1079
+ args: ""
1080
+ });
1081
+ } else if (ev.type === "content_block_start" && ev.content_block?.type === "mcp_tool_use") {
1082
+ toolCalls.set(ev.index, {
1083
+ id: ev.content_block.id,
1084
+ name: ev.content_block.name,
1085
+ args: "",
1086
+ server_name: ev.content_block.server_name
1087
+ });
1088
+ } else if (ev.type === "content_block_start" && // case of non-stream
1089
+ ev.content_block?.type === "tool_result") {
1090
+ textBlocks.push({
1091
+ type: "tool_result",
1092
+ tool_use_id: ev.content_block.tool_use_id,
1093
+ content: ev.content_block.content ?? ""
1094
+ });
1095
+ } else if (ev.type === "content_block_start" && ev.content_block?.type === "mcp_tool_result") {
1096
+ textBlocks.push({
1097
+ type: "mcp_tool_result",
1098
+ tool_use_id: ev.content_block.tool_use_id,
1099
+ is_error: ev.content_block.is_error ?? false,
1100
+ content: ev.content_block.content ?? []
931
1101
  });
932
1102
  }
1103
+ if (ev.type === "content_block_delta" && ev.delta?.type === "input_json_delta") {
1104
+ const entry = toolCalls.get(ev.index);
1105
+ if (entry) entry.args += ev.delta.partial_json || "";
1106
+ }
1107
+ if (ev.type === "content_block_stop" && toolCalls.has(ev.index)) {
1108
+ const { id, name, args, server_name } = toolCalls.get(ev.index);
1109
+ if (server_name) {
1110
+ textBlocks.push({
1111
+ type: "mcp_tool_use",
1112
+ id,
1113
+ name,
1114
+ server_name,
1115
+ input: JSON.parse(args || "{}")
1116
+ });
1117
+ } else {
1118
+ textBlocks.push({
1119
+ type: "tool_use",
1120
+ id,
1121
+ name,
1122
+ input: JSON.parse(args || "{}")
1123
+ });
1124
+ }
1125
+ toolCalls.delete(ev.index);
1126
+ }
933
1127
  }
934
1128
  }
935
- const toolBlocks = Array.from(toolCallsMap.entries()).sort((a, b) => a[0] - b[0]).map(([_, e]) => ({
936
- type: "tool_use",
937
- id: e.id,
938
- name: e.name,
939
- input: JSON.parse(e.args || "{}")
940
- }));
941
- const blocks = [...textBlocks, ...toolBlocks];
942
1129
  return {
943
- blocks,
944
- stop_reason: toolBlocks.length ? "tool_use" : "end"
1130
+ blocks: textBlocks,
1131
+ stop_reason: textBlocks.some(
1132
+ (b) => b.type === "tool_use" || b.type === "mcp_tool_use"
1133
+ ) ? "tool_use" : "end"
945
1134
  };
946
1135
  }
1136
+ async parsePureStream(res, onPartial) {
1137
+ const { blocks } = await this.parseStream(res, onPartial);
1138
+ return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
1139
+ }
947
1140
  parseOneShot(data) {
948
- const choice = data.choices[0];
949
1141
  const blocks = [];
950
- if (choice.finish_reason === "tool_calls") {
951
- choice.message.tool_calls.forEach(
952
- (c) => blocks.push({
1142
+ (data.content ?? []).forEach((c) => {
1143
+ if (c.type === "text") {
1144
+ blocks.push({ type: "text", text: c.text });
1145
+ } else if (c.type === "tool_use") {
1146
+ blocks.push({
953
1147
  type: "tool_use",
954
1148
  id: c.id,
955
- name: c.function.name,
956
- input: JSON.parse(c.function.arguments || "{}")
957
- })
958
- );
959
- } else {
960
- blocks.push({ type: "text", text: choice.message.content });
961
- }
1149
+ name: c.name,
1150
+ input: c.input ?? {}
1151
+ });
1152
+ } else if (c.type === "mcp_tool_use") {
1153
+ blocks.push({
1154
+ type: "mcp_tool_use",
1155
+ id: c.id,
1156
+ name: c.name,
1157
+ server_name: c.server_name,
1158
+ input: c.input ?? {}
1159
+ });
1160
+ } else if (c.type === "tool_result") {
1161
+ blocks.push({
1162
+ type: "tool_result",
1163
+ tool_use_id: c.tool_use_id,
1164
+ content: c.content ?? ""
1165
+ });
1166
+ } else if (c.type === "mcp_tool_result") {
1167
+ blocks.push({
1168
+ type: "mcp_tool_result",
1169
+ tool_use_id: c.tool_use_id,
1170
+ is_error: c.is_error ?? false,
1171
+ content: c.content ?? []
1172
+ });
1173
+ }
1174
+ });
962
1175
  return {
963
1176
  blocks,
964
- stop_reason: choice.finish_reason === "tool_calls" ? "tool_use" : "end"
1177
+ stop_reason: blocks.some(
1178
+ (b) => b.type === "tool_use" || b.type === "mcp_tool_use"
1179
+ ) ? "tool_use" : "end"
965
1180
  };
966
1181
  }
967
1182
  /**
968
- * Parse streaming response from Responses API (SSE format)
1183
+ * Process chat messages
1184
+ * @param messages Array of messages to send
1185
+ * @param stream Whether to stream the response
1186
+ * @param onPartial Callback to receive each part of streaming response
1187
+ * @param maxTokens Maximum tokens for response (optional)
1188
+ * @returns ToolChatCompletion
969
1189
  */
970
- async parseResponsesStream(res, onPartial) {
971
- const reader = res.body.getReader();
972
- const dec = new TextDecoder();
973
- const textBlocks = [];
974
- const toolCallsMap = /* @__PURE__ */ new Map();
975
- let buf = "";
976
- while (true) {
977
- const { done, value } = await reader.read();
978
- if (done) break;
979
- buf += dec.decode(value, { stream: true });
980
- let eventType = "";
981
- let eventData = "";
982
- const lines = buf.split("\n");
983
- buf = lines.pop() || "";
984
- for (let i = 0; i < lines.length; i++) {
985
- const line = lines[i].trim();
986
- if (line.startsWith("event:")) {
987
- eventType = line.slice(6).trim();
988
- } else if (line.startsWith("data:")) {
989
- eventData = line.slice(5).trim();
990
- } else if (line === "" && eventType && eventData) {
991
- try {
992
- const json = JSON.parse(eventData);
993
- const completionResult = this.handleResponsesSSEEvent(
994
- eventType,
995
- json,
996
- onPartial,
997
- textBlocks,
998
- toolCallsMap
999
- );
1000
- if (completionResult === "completed") {
1001
- }
1002
- } catch (e) {
1003
- console.warn("Failed to parse SSE data:", eventData);
1004
- }
1005
- eventType = "";
1006
- eventData = "";
1007
- }
1008
- }
1009
- }
1010
- const toolBlocks = Array.from(toolCallsMap.values()).map(
1011
- (tool) => ({
1012
- type: "tool_use",
1013
- id: tool.id,
1014
- name: tool.name,
1015
- input: tool.input || {}
1016
- })
1017
- );
1018
- const blocks = [...textBlocks, ...toolBlocks];
1019
- return {
1020
- blocks,
1021
- stop_reason: toolBlocks.length ? "tool_use" : "end"
1022
- };
1190
+ async chatOnce(messages, stream = true, onPartial = () => {
1191
+ }, maxTokens) {
1192
+ const res = await this.callClaude(messages, this.model, stream, maxTokens);
1193
+ const internalResult = stream ? await this.parseStream(res, onPartial) : this.parseOneShot(await res.json());
1194
+ return this.convertToStandardCompletion(internalResult);
1023
1195
  }
1024
1196
  /**
1025
- * Handle specific SSE events from Responses API
1026
- * @returns 'completed' if the response is completed, undefined otherwise
1197
+ * Process vision chat messages
1198
+ * @param messages Array of messages to send
1199
+ * @param stream Whether to stream the response
1200
+ * @param onPartial Callback to receive each part of streaming response
1201
+ * @param maxTokens Maximum tokens for response (optional)
1202
+ * @returns ToolChatCompletion
1027
1203
  */
1028
- handleResponsesSSEEvent(eventType, data, onPartial, textBlocks, toolCallsMap) {
1029
- switch (eventType) {
1030
- // Item addition events
1031
- case "response.output_item.added":
1032
- if (data.item?.type === "message" && Array.isArray(data.item.content)) {
1033
- data.item.content.forEach((c) => {
1034
- if (c.type === "output_text" && c.text) {
1035
- onPartial(c.text);
1036
- StreamTextAccumulator.append(textBlocks, c.text);
1037
- }
1038
- });
1039
- } else if (data.item?.type === "function_call") {
1040
- toolCallsMap.set(data.item.id, {
1041
- id: data.item.id,
1042
- name: data.item.name,
1043
- input: data.item.arguments ? JSON.parse(data.item.arguments) : {}
1044
- });
1045
- }
1046
- break;
1047
- // Initial content part events
1048
- case "response.content_part.added":
1049
- if (data.part?.type === "output_text" && typeof data.part.text === "string") {
1050
- onPartial(data.part.text);
1051
- StreamTextAccumulator.append(textBlocks, data.part.text);
1052
- }
1053
- break;
1054
- // Text delta events
1055
- case "response.output_text.delta":
1056
- case "response.content_part.delta":
1057
- {
1058
- const deltaText = typeof data.delta === "string" ? data.delta : data.delta?.text ?? "";
1059
- if (deltaText) {
1060
- onPartial(deltaText);
1061
- StreamTextAccumulator.append(textBlocks, deltaText);
1062
- }
1063
- }
1064
- break;
1065
- // Text completion events - do not add text here as it's already accumulated via delta events
1066
- case "response.output_text.done":
1067
- case "response.content_part.done":
1068
- break;
1069
- // Response completion events
1070
- case "response.completed":
1071
- return "completed";
1072
- // GPT-5 reasoning token events (not visible but counted for billing)
1073
- case "response.reasoning.started":
1074
- case "response.reasoning.delta":
1075
- case "response.reasoning.done":
1076
- break;
1077
- default:
1078
- break;
1079
- }
1080
- return void 0;
1204
+ async visionChatOnce(messages, stream = false, onPartial = () => {
1205
+ }, maxTokens) {
1206
+ const res = await this.callClaude(
1207
+ messages,
1208
+ this.visionModel,
1209
+ stream,
1210
+ maxTokens
1211
+ );
1212
+ const internalResult = stream ? await this.parseStream(res, onPartial) : this.parseOneShot(await res.json());
1213
+ return this.convertToStandardCompletion(internalResult);
1081
1214
  }
1082
1215
  /**
1083
- * Parse non-streaming response from Responses API
1216
+ * Convert internal completion to standard ToolChatCompletion
1217
+ * @param completion Internal completion result
1218
+ * @returns Standard ToolChatCompletion
1084
1219
  */
1085
- parseResponsesOneShot(data) {
1086
- const blocks = [];
1087
- if (data.output && Array.isArray(data.output)) {
1088
- data.output.forEach((outputItem) => {
1089
- if (outputItem.type === "message" && outputItem.content) {
1090
- outputItem.content.forEach((content) => {
1091
- if (content.type === "output_text" && content.text) {
1092
- blocks.push({ type: "text", text: content.text });
1093
- }
1094
- });
1095
- }
1096
- if (outputItem.type === "function_call") {
1097
- blocks.push({
1098
- type: "tool_use",
1099
- id: outputItem.id,
1100
- name: outputItem.name,
1101
- input: outputItem.arguments ? JSON.parse(outputItem.arguments) : {}
1102
- });
1103
- }
1104
- });
1105
- }
1220
+ convertToStandardCompletion(completion) {
1221
+ const standardBlocks = completion.blocks.filter(
1222
+ (block) => {
1223
+ return block.type === "text" || block.type === "tool_use" || block.type === "tool_result";
1224
+ }
1225
+ );
1106
1226
  return {
1107
- blocks,
1108
- stop_reason: blocks.some((b) => b.type === "tool_use") ? "tool_use" : "end"
1227
+ blocks: standardBlocks,
1228
+ stop_reason: completion.stop_reason
1109
1229
  };
1110
1230
  }
1111
1231
  };
1112
1232
 
1113
- // src/services/providers/openai/OpenAIChatServiceProvider.ts
1114
- var OpenAIChatServiceProvider = class {
1233
+ // src/services/providers/claude/ClaudeChatServiceProvider.ts
1234
+ var ClaudeChatServiceProvider = class {
1115
1235
  /**
1116
1236
  * Create a chat service instance
1117
- * @param options Service options
1118
- * @returns OpenAIChatService instance
1237
+ * @param options Service options (can include mcpServers)
1238
+ * @returns ClaudeChatService instance
1119
1239
  */
1120
1240
  createChatService(options) {
1121
- const optimizedOptions = this.optimizeGPT5Options(options);
1122
- const visionModel = optimizedOptions.visionModel || (this.supportsVisionForModel(
1123
- optimizedOptions.model || this.getDefaultModel()
1124
- ) ? optimizedOptions.model : this.getDefaultModel());
1125
- const tools = optimizedOptions.tools;
1126
- const mcpServers = optimizedOptions.mcpServers ?? [];
1127
- const modelName = optimizedOptions.model || this.getDefaultModel();
1128
- let shouldUseResponsesAPI = false;
1129
- if (mcpServers.length > 0) {
1130
- shouldUseResponsesAPI = true;
1131
- } else if (isGPT5Model(modelName)) {
1132
- const preference = optimizedOptions.gpt5EndpointPreference || "chat";
1133
- shouldUseResponsesAPI = preference === "responses";
1134
- }
1135
- const endpoint = optimizedOptions.endpoint || (shouldUseResponsesAPI ? ENDPOINT_OPENAI_RESPONSES_API : ENDPOINT_OPENAI_CHAT_COMPLETIONS_API);
1136
- return new OpenAIChatService(
1137
- optimizedOptions.apiKey,
1138
- modelName,
1241
+ const visionModel = resolveVisionModel({
1242
+ model: options.model,
1243
+ visionModel: options.visionModel,
1244
+ defaultModel: this.getDefaultModel(),
1245
+ defaultVisionModel: this.getDefaultModel(),
1246
+ supportsVisionForModel: (model) => this.supportsVisionForModel(model),
1247
+ validate: "resolved"
1248
+ });
1249
+ return new ClaudeChatService(
1250
+ options.apiKey,
1251
+ options.model || this.getDefaultModel(),
1139
1252
  visionModel,
1140
- tools,
1141
- endpoint,
1142
- mcpServers,
1143
- optimizedOptions.responseLength,
1144
- optimizedOptions.verbosity,
1145
- optimizedOptions.reasoning_effort,
1146
- optimizedOptions.enableReasoningSummary
1253
+ options.tools ?? [],
1254
+ options.mcpServers ?? [],
1255
+ options.responseLength
1147
1256
  );
1148
1257
  }
1149
1258
  /**
1150
1259
  * Get the provider name
1151
- * @returns Provider name ('openai')
1260
+ * @returns Provider name ('claude')
1152
1261
  */
1153
1262
  getProviderName() {
1154
- return "openai";
1263
+ return "claude";
1155
1264
  }
1156
1265
  /**
1157
1266
  * Get the list of supported models
@@ -1159,19 +1268,16 @@ If it's in another language, summarize in that language.
1159
1268
  */
1160
1269
  getSupportedModels() {
1161
1270
  return [
1162
- MODEL_GPT_5_NANO,
1163
- MODEL_GPT_5_MINI,
1164
- MODEL_GPT_5,
1165
- MODEL_GPT_5_1,
1166
- MODEL_GPT_4_1,
1167
- MODEL_GPT_4_1_MINI,
1168
- MODEL_GPT_4_1_NANO,
1169
- MODEL_GPT_4O_MINI,
1170
- MODEL_GPT_4O,
1171
- MODEL_O3_MINI,
1172
- MODEL_O1_MINI,
1173
- MODEL_O1,
1174
- MODEL_GPT_4_5_PREVIEW
1271
+ MODEL_CLAUDE_3_HAIKU,
1272
+ MODEL_CLAUDE_3_5_HAIKU,
1273
+ MODEL_CLAUDE_3_5_SONNET,
1274
+ MODEL_CLAUDE_3_7_SONNET,
1275
+ MODEL_CLAUDE_4_SONNET,
1276
+ MODEL_CLAUDE_4_OPUS,
1277
+ MODEL_CLAUDE_4_5_SONNET,
1278
+ MODEL_CLAUDE_4_5_HAIKU,
1279
+ MODEL_CLAUDE_4_5_OPUS,
1280
+ MODEL_CLAUDE_4_6_OPUS
1175
1281
  ];
1176
1282
  }
1177
1283
  /**
@@ -1179,7 +1285,7 @@ If it's in another language, summarize in that language.
1179
1285
  * @returns Default model name
1180
1286
  */
1181
1287
  getDefaultModel() {
1182
- return MODEL_GPT_5_NANO;
1288
+ return MODEL_CLAUDE_3_HAIKU;
1183
1289
  }
1184
1290
  /**
1185
1291
  * Check if this provider supports vision (image processing)
@@ -1194,55 +1300,7 @@ If it's in another language, summarize in that language.
1194
1300
  * @returns True if the model supports vision, false otherwise
1195
1301
  */
1196
1302
  supportsVisionForModel(model) {
1197
- return VISION_SUPPORTED_MODELS.includes(model);
1198
- }
1199
- /**
1200
- * Apply GPT-5 specific optimizations to options
1201
- * @param options Original chat service options
1202
- * @returns Optimized options for GPT-5 usage
1203
- */
1204
- optimizeGPT5Options(options) {
1205
- const modelName = options.model || this.getDefaultModel();
1206
- if (!isGPT5Model(modelName)) {
1207
- return options;
1208
- }
1209
- const optimized = { ...options };
1210
- if (options.gpt5Preset) {
1211
- const preset = GPT5_PRESETS[options.gpt5Preset];
1212
- optimized.reasoning_effort = preset.reasoning_effort;
1213
- optimized.verbosity = preset.verbosity;
1214
- } else {
1215
- if (!options.reasoning_effort) {
1216
- optimized.reasoning_effort = this.getDefaultReasoningEffortForModel(modelName);
1217
- }
1218
- }
1219
- optimized.reasoning_effort = this.normalizeReasoningEffort(
1220
- modelName,
1221
- optimized.reasoning_effort
1222
- );
1223
- return optimized;
1224
- }
1225
- /**
1226
- * Determine the default reasoning effort for GPT-5 family models
1227
- * GPT-5.1 defaults to 'none' (fastest), earlier GPT-5 defaults to 'medium'
1228
- */
1229
- getDefaultReasoningEffortForModel(modelName) {
1230
- if (modelName === MODEL_GPT_5_1) {
1231
- return "none";
1232
- }
1233
- return "medium";
1234
- }
1235
- normalizeReasoningEffort(modelName, effort) {
1236
- if (!effort) {
1237
- return void 0;
1238
- }
1239
- if (effort === "none" && !allowsReasoningNone(modelName)) {
1240
- return this.getDefaultReasoningEffortForModel(modelName);
1241
- }
1242
- if (effort === "minimal" && !allowsReasoningMinimal(modelName)) {
1243
- return "none";
1244
- }
1245
- return effort;
1303
+ return CLAUDE_VISION_SUPPORTED_MODELS.includes(model);
1246
1304
  }
1247
1305
  };
1248
1306
 
@@ -1495,26 +1553,17 @@ If it's in another language, summarize in that language.
1495
1553
  */
1496
1554
  async processChat(messages, onPartialResponse, onCompleteResponse) {
1497
1555
  try {
1498
- if (this.tools.length === 0 && this.mcpServers.length === 0) {
1499
- const res = await this.callGemini(messages, this.model, true);
1500
- const { blocks: blocks2 } = await this.parseStream(res, onPartialResponse);
1501
- const full = blocks2.filter((b) => b.type === "text").map((b) => b.text).join("");
1502
- await onCompleteResponse(full);
1503
- return;
1504
- }
1505
- const { blocks, stop_reason } = await this.chatOnce(
1506
- messages,
1507
- true,
1508
- onPartialResponse
1509
- );
1510
- if (stop_reason === "end") {
1511
- const full = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
1512
- await onCompleteResponse(full);
1513
- return;
1514
- }
1515
- throw new Error(
1516
- "Received functionCall. Use chatOnce() loop when tools are enabled."
1517
- );
1556
+ await processChatWithOptionalTools({
1557
+ hasTools: this.tools.length > 0 || this.mcpServers.length > 0,
1558
+ runWithoutTools: async () => {
1559
+ const res = await this.callGemini(messages, this.model, true);
1560
+ const { blocks } = await this.parseStream(res, onPartialResponse);
1561
+ return StreamTextAccumulator.getFullText(blocks);
1562
+ },
1563
+ runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
1564
+ onCompleteResponse,
1565
+ toolErrorMessage: "Received functionCall. Use chatOnce() loop when tools are enabled."
1566
+ });
1518
1567
  } catch (err) {
1519
1568
  console.error("Error in processChat:", err);
1520
1569
  throw err;
@@ -1522,23 +1571,22 @@ If it's in another language, summarize in that language.
1522
1571
  }
1523
1572
  async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
1524
1573
  try {
1525
- if (this.tools.length === 0 && this.mcpServers.length === 0) {
1526
- const res = await this.callGemini(messages, this.visionModel, true);
1527
- const { blocks: blocks2 } = await this.parseStream(res, onPartialResponse);
1528
- const full = blocks2.filter((b) => b.type === "text").map((b) => b.text).join("");
1529
- await onCompleteResponse(full);
1530
- return;
1531
- }
1532
- const { blocks, stop_reason } = await this.visionChatOnce(messages);
1533
- blocks.filter((b) => b.type === "text").forEach((b) => onPartialResponse(b.text));
1534
- if (stop_reason === "end") {
1535
- const full = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
1536
- await onCompleteResponse(full);
1537
- return;
1538
- }
1539
- throw new Error(
1540
- "Received functionCall. Use visionChatOnce() loop when tools are enabled."
1541
- );
1574
+ await processChatWithOptionalTools({
1575
+ hasTools: this.tools.length > 0 || this.mcpServers.length > 0,
1576
+ runWithoutTools: async () => {
1577
+ const res = await this.callGemini(messages, this.visionModel, true);
1578
+ const { blocks } = await this.parseStream(res, onPartialResponse);
1579
+ return StreamTextAccumulator.getFullText(blocks);
1580
+ },
1581
+ runWithTools: () => this.visionChatOnce(messages),
1582
+ onToolBlocks: (blocks) => {
1583
+ blocks.filter(
1584
+ (b) => b.type === "text"
1585
+ ).forEach((b) => onPartialResponse(b.text));
1586
+ },
1587
+ onCompleteResponse,
1588
+ toolErrorMessage: "Received functionCall. Use visionChatOnce() loop when tools are enabled."
1589
+ });
1542
1590
  } catch (err) {
1543
1591
  console.error("Error in processVisionChat:", err);
1544
1592
  throw err;
@@ -1943,7 +1991,14 @@ If it's in another language, summarize in that language.
1943
1991
  * @returns GeminiChatService instance
1944
1992
  */
1945
1993
  createChatService(options) {
1946
- const visionModel = options.visionModel || (this.supportsVisionForModel(options.model || this.getDefaultModel()) ? options.model : this.getDefaultModel());
1994
+ const visionModel = resolveVisionModel({
1995
+ model: options.model,
1996
+ visionModel: options.visionModel,
1997
+ defaultModel: this.getDefaultModel(),
1998
+ defaultVisionModel: this.getDefaultModel(),
1999
+ supportsVisionForModel: (model) => this.supportsVisionForModel(model),
2000
+ validate: "resolved"
2001
+ });
1947
2002
  return new GeminiChatService(
1948
2003
  options.apiKey,
1949
2004
  options.model || this.getDefaultModel(),
@@ -1998,563 +2053,281 @@ If it's in another language, summarize in that language.
1998
2053
  }
1999
2054
  };
2000
2055
 
2001
- // src/services/providers/claude/ClaudeChatService.ts
2002
- var ClaudeChatService = class {
2056
+ // src/services/providers/kimi/KimiChatService.ts
2057
+ var KimiChatService = class {
2003
2058
  /**
2004
2059
  * Constructor
2005
- * @param apiKey Anthropic API key
2060
+ * @param apiKey Kimi API key
2006
2061
  * @param model Name of the model to use
2007
2062
  * @param visionModel Name of the vision model
2008
- * @param tools Array of tool definitions
2009
- * @param mcpServers Array of MCP server configurations (optional)
2010
- * @throws Error if the vision model doesn't support vision capabilities
2011
2063
  */
2012
- constructor(apiKey, model = MODEL_CLAUDE_3_HAIKU, visionModel = MODEL_CLAUDE_3_HAIKU, tools = [], mcpServers = [], responseLength) {
2064
+ constructor(apiKey, model = MODEL_KIMI_K2_5, visionModel = MODEL_KIMI_K2_5, tools, endpoint = ENDPOINT_KIMI_CHAT_COMPLETIONS_API, responseLength, responseFormat, thinking) {
2013
2065
  /** Provider name */
2014
- this.provider = "claude";
2066
+ this.provider = "kimi";
2015
2067
  this.apiKey = apiKey;
2016
- this.model = model || MODEL_CLAUDE_3_HAIKU;
2017
- this.visionModel = visionModel || MODEL_CLAUDE_3_HAIKU;
2018
- this.tools = tools;
2019
- this.mcpServers = mcpServers;
2068
+ this.model = model;
2069
+ this.tools = tools || [];
2070
+ this.endpoint = endpoint;
2020
2071
  this.responseLength = responseLength;
2021
- if (!CLAUDE_VISION_SUPPORTED_MODELS.includes(this.visionModel)) {
2022
- throw new Error(
2023
- `Model ${this.visionModel} does not support vision capabilities.`
2024
- );
2025
- }
2072
+ this.responseFormat = responseFormat;
2073
+ this.thinking = thinking ?? { type: "enabled" };
2074
+ this.visionModel = visionModel;
2026
2075
  }
2027
2076
  /**
2028
2077
  * Get the current model name
2029
- * @returns Model name
2030
2078
  */
2031
2079
  getModel() {
2032
2080
  return this.model;
2033
2081
  }
2034
2082
  /**
2035
2083
  * Get the current vision model name
2036
- * @returns Vision model name
2037
2084
  */
2038
2085
  getVisionModel() {
2039
2086
  return this.visionModel;
2040
2087
  }
2041
- /**
2042
- * Get configured MCP servers
2043
- * @returns Array of MCP server configurations
2044
- */
2045
- getMCPServers() {
2046
- return this.mcpServers;
2047
- }
2048
- /**
2049
- * Add MCP server configuration
2050
- * @param serverConfig MCP server configuration
2051
- */
2052
- addMCPServer(serverConfig) {
2053
- this.mcpServers.push(serverConfig);
2054
- }
2055
- /**
2056
- * Remove MCP server by name
2057
- * @param serverName Name of the server to remove
2058
- */
2059
- removeMCPServer(serverName) {
2060
- this.mcpServers = this.mcpServers.filter(
2061
- (server) => server.name !== serverName
2062
- );
2063
- }
2064
- /**
2065
- * Check if MCP servers are configured
2066
- * @returns True if MCP servers are configured
2067
- */
2068
- hasMCPServers() {
2069
- return this.mcpServers.length > 0;
2070
- }
2071
2088
  /**
2072
2089
  * Process chat messages
2073
- * @param messages Array of messages to send
2074
- * @param onPartialResponse Callback to receive each part of streaming response
2075
- * @param onCompleteResponse Callback to execute when response is complete
2076
2090
  */
2077
2091
  async processChat(messages, onPartialResponse, onCompleteResponse) {
2078
- if (this.tools.length === 0 && this.mcpServers.length === 0) {
2079
- const res = await this.callClaude(messages, this.model, true);
2080
- const full = await this.parsePureStream(res, onPartialResponse);
2081
- await onCompleteResponse(full);
2082
- return;
2083
- }
2084
- const result = await this.chatOnce(messages, true, onPartialResponse);
2085
- if (result.stop_reason === "end") {
2086
- const full = result.blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2087
- await onCompleteResponse(full);
2088
- return;
2089
- }
2090
- throw new Error(
2091
- "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
2092
- );
2092
+ await processChatWithOptionalTools({
2093
+ hasTools: this.tools.length > 0,
2094
+ runWithoutTools: async () => {
2095
+ const res = await this.callKimi(messages, this.model, true);
2096
+ return this.handleStream(res, onPartialResponse);
2097
+ },
2098
+ runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
2099
+ onCompleteResponse,
2100
+ toolErrorMessage: "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
2101
+ });
2093
2102
  }
2094
2103
  /**
2095
2104
  * Process chat messages with images
2096
- * @param messages Array of messages to send (including images)
2097
- * @param onPartialResponse Callback to receive each part of streaming response
2098
- * @param onCompleteResponse Callback to execute when response is complete
2099
2105
  */
2100
2106
  async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
2101
- if (this.tools.length === 0 && this.mcpServers.length === 0) {
2102
- const res = await this.callClaude(messages, this.visionModel, true);
2103
- const full = await this.parsePureStream(res, onPartialResponse);
2104
- await onCompleteResponse(full);
2105
- return;
2106
- }
2107
- const result = await this.visionChatOnce(messages);
2108
- if (result.stop_reason === "end") {
2109
- const full = result.blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2110
- await onCompleteResponse(full);
2111
- return;
2107
+ if (!isKimiVisionModel(this.visionModel)) {
2108
+ throw new Error(
2109
+ `Model ${this.visionModel} does not support vision capabilities.`
2110
+ );
2112
2111
  }
2113
- throw new Error(
2114
- "processVisionChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
2115
- );
2116
- }
2117
- /**
2118
- * Convert AITuber OnAir messages to Claude format
2119
- * @param messages Array of messages
2120
- * @returns Claude formatted messages
2121
- */
2122
- convertMessagesToClaudeFormat(messages) {
2123
- return messages.map((msg) => {
2124
- return {
2125
- role: this.mapRoleToClaude(msg.role),
2126
- content: msg.content
2127
- };
2112
+ await processChatWithOptionalTools({
2113
+ hasTools: this.tools.length > 0,
2114
+ runWithoutTools: async () => {
2115
+ const res = await this.callKimi(messages, this.visionModel, true);
2116
+ return this.handleStream(res, onPartialResponse);
2117
+ },
2118
+ runWithTools: () => this.visionChatOnce(messages, true, onPartialResponse),
2119
+ onCompleteResponse,
2120
+ toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
2128
2121
  });
2129
2122
  }
2130
2123
  /**
2131
- * Convert AITuber OnAir vision messages to Claude format
2132
- * @param messages Array of vision messages
2133
- * @returns Claude formatted vision messages
2124
+ * Process chat messages with tools (text only)
2134
2125
  */
2135
- convertVisionMessagesToClaudeFormat(messages) {
2136
- return messages.map((msg) => {
2137
- if (typeof msg.content === "string") {
2138
- return {
2139
- role: this.mapRoleToClaude(msg.role),
2140
- content: [
2141
- {
2142
- type: "text",
2143
- text: msg.content
2144
- }
2145
- ]
2146
- };
2147
- }
2148
- if (Array.isArray(msg.content)) {
2149
- const content = msg.content.map((block) => {
2150
- if (block.type === "image_url") {
2151
- if (block.image_url.url.startsWith("data:")) {
2152
- const m = block.image_url.url.match(
2153
- /^data:([^;]+);base64,(.+)$/
2154
- );
2155
- if (m) {
2156
- return {
2157
- type: "image",
2158
- source: { type: "base64", media_type: m[1], data: m[2] }
2159
- };
2160
- }
2161
- return null;
2162
- }
2163
- return {
2164
- type: "image",
2165
- source: {
2166
- type: "url",
2167
- url: block.image_url.url,
2168
- media_type: this.getMimeTypeFromUrl(block.image_url.url)
2169
- }
2170
- };
2171
- }
2172
- return block;
2173
- }).filter((b) => b);
2174
- return {
2175
- role: this.mapRoleToClaude(msg.role),
2176
- content
2177
- };
2178
- }
2179
- return {
2180
- role: this.mapRoleToClaude(msg.role),
2181
- content: []
2182
- };
2183
- });
2126
+ async chatOnce(messages, stream = true, onPartialResponse = () => {
2127
+ }, maxTokens) {
2128
+ const res = await this.callKimi(messages, this.model, stream, maxTokens);
2129
+ return this.parseResponse(res, stream, onPartialResponse);
2184
2130
  }
2185
2131
  /**
2186
- * Map AITuber OnAir roles to Claude roles
2187
- * @param role AITuber OnAir role
2188
- * @returns Claude role
2132
+ * Process vision chat messages with tools
2189
2133
  */
2190
- mapRoleToClaude(role) {
2191
- switch (role) {
2192
- case "system":
2193
- return "system";
2194
- case "user":
2195
- return "user";
2196
- case "assistant":
2197
- return "assistant";
2198
- default:
2199
- return "user";
2134
+ async visionChatOnce(messages, stream = false, onPartialResponse = () => {
2135
+ }, maxTokens) {
2136
+ if (!isKimiVisionModel(this.visionModel)) {
2137
+ throw new Error(
2138
+ `Model ${this.visionModel} does not support vision capabilities.`
2139
+ );
2200
2140
  }
2141
+ const res = await this.callKimi(
2142
+ messages,
2143
+ this.visionModel,
2144
+ stream,
2145
+ maxTokens
2146
+ );
2147
+ return this.parseResponse(res, stream, onPartialResponse);
2201
2148
  }
2202
- /**
2203
- * Get MIME type from URL
2204
- * @param url Image URL
2205
- * @returns MIME type
2206
- */
2207
- getMimeTypeFromUrl(url) {
2208
- const extension = url.split(".").pop()?.toLowerCase();
2209
- switch (extension) {
2210
- case "jpg":
2211
- case "jpeg":
2212
- return "image/jpeg";
2213
- case "png":
2214
- return "image/png";
2215
- case "gif":
2216
- return "image/gif";
2217
- case "webp":
2218
- return "image/webp";
2219
- default:
2220
- return "image/jpeg";
2221
- }
2149
+ async parseResponse(res, stream, onPartialResponse) {
2150
+ return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
2151
+ }
2152
+ async callKimi(messages, model, stream = false, maxTokens) {
2153
+ const body = this.buildRequestBody(messages, model, stream, maxTokens);
2154
+ const res = await ChatServiceHttpClient.post(this.endpoint, body, {
2155
+ Authorization: `Bearer ${this.apiKey}`
2156
+ });
2157
+ return res;
2222
2158
  }
2223
2159
  /**
2224
- * Call Claude API
2225
- * @param messages Array of messages to send
2226
- * @param model Model name
2227
- * @param stream Whether to stream the response
2228
- * @param maxTokens Maximum tokens for response (optional)
2229
- * @returns Response
2160
+ * Build request body (OpenAI-compatible Chat Completions)
2230
2161
  */
2231
- async callClaude(messages, model, stream, maxTokens) {
2232
- const system = messages.find((m) => m.role === "system")?.content ?? "";
2233
- const content = messages.filter((m) => m.role !== "system");
2234
- const hasVision = content.some(
2235
- (m) => Array.isArray(m.content) && m.content.some(
2236
- (b) => b.type === "image_url" || b.type === "image"
2237
- )
2238
- );
2162
+ buildRequestBody(messages, model, stream, maxTokens) {
2239
2163
  const body = {
2240
2164
  model,
2241
- system,
2242
- messages: hasVision ? this.convertVisionMessagesToClaudeFormat(
2243
- content
2244
- ) : this.convertMessagesToClaudeFormat(content),
2245
2165
  stream,
2246
- max_tokens: maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength)
2166
+ messages
2247
2167
  };
2248
- if (this.tools.length) {
2249
- body.tools = this.tools.map((t) => ({
2250
- name: t.name,
2251
- description: t.description,
2252
- input_schema: t.parameters
2253
- }));
2254
- body.tool_choice = { type: "auto" };
2255
- }
2256
- if (this.mcpServers.length > 0) {
2257
- body.mcp_servers = this.mcpServers;
2168
+ const tokenLimit = maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength);
2169
+ if (tokenLimit !== void 0) {
2170
+ body.max_tokens = tokenLimit;
2258
2171
  }
2259
- const headers = {
2260
- "Content-Type": "application/json",
2261
- "x-api-key": this.apiKey,
2262
- "anthropic-version": "2023-06-01",
2263
- "anthropic-dangerous-direct-browser-access": "true"
2264
- };
2265
- if (this.mcpServers.length > 0) {
2266
- headers["anthropic-beta"] = "mcp-client-2025-04-04";
2172
+ if (this.responseFormat) {
2173
+ body.response_format = this.responseFormat;
2267
2174
  }
2268
- const res = await ChatServiceHttpClient.post(
2269
- ENDPOINT_CLAUDE_API,
2270
- body,
2271
- headers
2272
- );
2273
- return res;
2274
- }
2275
- /**
2276
- * Parse stream response
2277
- * @param res Response
2278
- * @param onPartial Callback to receive each part of streaming response
2279
- * @returns ClaudeInternalCompletion
2280
- */
2281
- async parseStream(res, onPartial) {
2282
- const reader = res.body.getReader();
2283
- const dec = new TextDecoder();
2284
- const textBlocks = [];
2285
- const toolCalls = /* @__PURE__ */ new Map();
2286
- let buf = "";
2287
- while (true) {
2288
- const { done, value } = await reader.read();
2289
- if (done) break;
2290
- buf += dec.decode(value, { stream: true });
2291
- let nl;
2292
- while ((nl = buf.indexOf("\n")) !== -1) {
2293
- const line = buf.slice(0, nl).trim();
2294
- buf = buf.slice(nl + 1);
2295
- if (!line.startsWith("data:")) continue;
2296
- const payload = line.slice(5).trim();
2297
- if (payload === "[DONE]") break;
2298
- const ev = JSON.parse(payload);
2299
- if (ev.type === "content_block_delta" && ev.delta?.text) {
2300
- onPartial(ev.delta.text);
2301
- textBlocks.push({ type: "text", text: ev.delta.text });
2302
- }
2303
- if (ev.type === "content_block_start" && ev.content_block?.type === "tool_use") {
2304
- toolCalls.set(ev.index, {
2305
- id: ev.content_block.id,
2306
- name: ev.content_block.name,
2307
- args: ""
2308
- });
2309
- } else if (ev.type === "content_block_start" && ev.content_block?.type === "mcp_tool_use") {
2310
- toolCalls.set(ev.index, {
2311
- id: ev.content_block.id,
2312
- name: ev.content_block.name,
2313
- args: "",
2314
- server_name: ev.content_block.server_name
2315
- });
2316
- } else if (ev.type === "content_block_start" && // case of non-stream
2317
- ev.content_block?.type === "tool_result") {
2318
- textBlocks.push({
2319
- type: "tool_result",
2320
- tool_use_id: ev.content_block.tool_use_id,
2321
- content: ev.content_block.content ?? ""
2322
- });
2323
- } else if (ev.type === "content_block_start" && ev.content_block?.type === "mcp_tool_result") {
2324
- textBlocks.push({
2325
- type: "mcp_tool_result",
2326
- tool_use_id: ev.content_block.tool_use_id,
2327
- is_error: ev.content_block.is_error ?? false,
2328
- content: ev.content_block.content ?? []
2329
- });
2330
- }
2331
- if (ev.type === "content_block_delta" && ev.delta?.type === "input_json_delta") {
2332
- const entry = toolCalls.get(ev.index);
2333
- if (entry) entry.args += ev.delta.partial_json || "";
2334
- }
2335
- if (ev.type === "content_block_stop" && toolCalls.has(ev.index)) {
2336
- const { id, name, args, server_name } = toolCalls.get(ev.index);
2337
- if (server_name) {
2338
- textBlocks.push({
2339
- type: "mcp_tool_use",
2340
- id,
2341
- name,
2342
- server_name,
2343
- input: JSON.parse(args || "{}")
2344
- });
2345
- } else {
2346
- textBlocks.push({
2347
- type: "tool_use",
2348
- id,
2349
- name,
2350
- input: JSON.parse(args || "{}")
2351
- });
2352
- }
2353
- toolCalls.delete(ev.index);
2175
+ const effectiveThinking = this.tools.length > 0 ? { type: "disabled" } : this.thinking;
2176
+ if (effectiveThinking) {
2177
+ if (this.isSelfHostedEndpoint()) {
2178
+ if (effectiveThinking.type === "disabled") {
2179
+ body.chat_template_kwargs = { thinking: false };
2354
2180
  }
2181
+ } else {
2182
+ body.thinking = effectiveThinking;
2355
2183
  }
2356
2184
  }
2357
- return {
2358
- blocks: textBlocks,
2359
- stop_reason: textBlocks.some(
2360
- (b) => b.type === "tool_use" || b.type === "mcp_tool_use"
2361
- ) ? "tool_use" : "end"
2362
- };
2363
- }
2364
- async parsePureStream(res, onPartial) {
2365
- const { blocks } = await this.parseStream(res, onPartial);
2366
- return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2367
- }
2368
- parseOneShot(data) {
2369
- const blocks = [];
2370
- (data.content ?? []).forEach((c) => {
2371
- if (c.type === "text") {
2372
- blocks.push({ type: "text", text: c.text });
2373
- } else if (c.type === "tool_use") {
2374
- blocks.push({
2375
- type: "tool_use",
2376
- id: c.id,
2377
- name: c.name,
2378
- input: c.input ?? {}
2379
- });
2380
- } else if (c.type === "mcp_tool_use") {
2381
- blocks.push({
2382
- type: "mcp_tool_use",
2383
- id: c.id,
2384
- name: c.name,
2385
- server_name: c.server_name,
2386
- input: c.input ?? {}
2387
- });
2388
- } else if (c.type === "tool_result") {
2389
- blocks.push({
2390
- type: "tool_result",
2391
- tool_use_id: c.tool_use_id,
2392
- content: c.content ?? ""
2393
- });
2394
- } else if (c.type === "mcp_tool_result") {
2395
- blocks.push({
2396
- type: "mcp_tool_result",
2397
- tool_use_id: c.tool_use_id,
2398
- is_error: c.is_error ?? false,
2399
- content: c.content ?? []
2400
- });
2401
- }
2402
- });
2403
- return {
2404
- blocks,
2405
- stop_reason: blocks.some(
2406
- (b) => b.type === "tool_use" || b.type === "mcp_tool_use"
2407
- ) ? "tool_use" : "end"
2408
- };
2409
- }
2410
- /**
2411
- * Process chat messages
2412
- * @param messages Array of messages to send
2413
- * @param stream Whether to stream the response
2414
- * @param onPartial Callback to receive each part of streaming response
2415
- * @param maxTokens Maximum tokens for response (optional)
2416
- * @returns ToolChatCompletion
2417
- */
2418
- async chatOnce(messages, stream = true, onPartial = () => {
2419
- }, maxTokens) {
2420
- const res = await this.callClaude(messages, this.model, stream, maxTokens);
2421
- const internalResult = stream ? await this.parseStream(res, onPartial) : this.parseOneShot(await res.json());
2422
- return this.convertToStandardCompletion(internalResult);
2185
+ const tools = this.buildToolsDefinition();
2186
+ if (tools.length > 0) {
2187
+ body.tools = tools;
2188
+ body.tool_choice = "auto";
2189
+ }
2190
+ return body;
2191
+ }
2192
+ isSelfHostedEndpoint() {
2193
+ return this.normalizeEndpoint(this.endpoint) !== this.normalizeEndpoint(ENDPOINT_KIMI_CHAT_COMPLETIONS_API);
2194
+ }
2195
+ normalizeEndpoint(value) {
2196
+ return value.replace(/\/+$/, "");
2197
+ }
2198
+ buildToolsDefinition() {
2199
+ return buildOpenAICompatibleTools(this.tools, "chat-completions");
2200
+ }
2201
+ async handleStream(res, onPartial) {
2202
+ return parseOpenAICompatibleTextStream(res, onPartial, {
2203
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
2204
+ });
2423
2205
  }
2424
2206
  /**
2425
- * Process vision chat messages
2426
- * @param messages Array of messages to send
2427
- * @param stream Whether to stream the response
2428
- * @param onPartial Callback to receive each part of streaming response
2429
- * @param maxTokens Maximum tokens for response (optional)
2430
- * @returns ToolChatCompletion
2207
+ * Parse streaming response with tool support
2431
2208
  */
2432
- async visionChatOnce(messages, stream = false, onPartial = () => {
2433
- }, maxTokens) {
2434
- const res = await this.callClaude(
2435
- messages,
2436
- this.visionModel,
2437
- stream,
2438
- maxTokens
2439
- );
2440
- const internalResult = stream ? await this.parseStream(res, onPartial) : this.parseOneShot(await res.json());
2441
- return this.convertToStandardCompletion(internalResult);
2209
+ async parseStream(res, onPartial) {
2210
+ return parseOpenAICompatibleToolStream(res, onPartial, {
2211
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
2212
+ });
2442
2213
  }
2443
2214
  /**
2444
- * Convert internal completion to standard ToolChatCompletion
2445
- * @param completion Internal completion result
2446
- * @returns Standard ToolChatCompletion
2215
+ * Parse non-streaming response
2447
2216
  */
2448
- convertToStandardCompletion(completion) {
2449
- const standardBlocks = completion.blocks.filter(
2450
- (block) => {
2451
- return block.type === "text" || block.type === "tool_use" || block.type === "tool_result";
2452
- }
2453
- );
2454
- return {
2455
- blocks: standardBlocks,
2456
- stop_reason: completion.stop_reason
2457
- };
2217
+ parseOneShot(data) {
2218
+ return parseOpenAICompatibleOneShot(data);
2458
2219
  }
2459
2220
  };
2460
2221
 
2461
- // src/services/providers/claude/ClaudeChatServiceProvider.ts
2462
- var ClaudeChatServiceProvider = class {
2222
+ // src/services/providers/kimi/KimiChatServiceProvider.ts
2223
+ var KimiChatServiceProvider = class {
2463
2224
  /**
2464
2225
  * Create a chat service instance
2465
- * @param options Service options (can include mcpServers)
2466
- * @returns ClaudeChatService instance
2467
2226
  */
2468
2227
  createChatService(options) {
2469
- const visionModel = options.visionModel || (this.supportsVisionForModel(options.model || this.getDefaultModel()) ? options.model : this.getDefaultModel());
2470
- return new ClaudeChatService(
2228
+ const endpoint = this.resolveEndpoint(options);
2229
+ const model = options.model || this.getDefaultModel();
2230
+ const visionModel = resolveVisionModel({
2231
+ model,
2232
+ visionModel: options.visionModel,
2233
+ defaultModel: this.getDefaultModel(),
2234
+ defaultVisionModel: this.getDefaultVisionModel(),
2235
+ supportsVisionForModel: (visionModel2) => this.supportsVisionForModel(visionModel2),
2236
+ validate: "explicit"
2237
+ });
2238
+ const tools = options.tools;
2239
+ const defaultThinking = options.thinking ?? { type: "enabled" };
2240
+ const thinking = tools && tools.length > 0 ? { type: "disabled" } : defaultThinking;
2241
+ return new KimiChatService(
2471
2242
  options.apiKey,
2472
- options.model || this.getDefaultModel(),
2243
+ model,
2473
2244
  visionModel,
2474
- options.tools ?? [],
2475
- options.mcpServers ?? [],
2476
- options.responseLength
2245
+ tools,
2246
+ endpoint,
2247
+ options.responseLength,
2248
+ options.responseFormat,
2249
+ thinking
2477
2250
  );
2478
2251
  }
2479
2252
  /**
2480
2253
  * Get the provider name
2481
- * @returns Provider name ('claude')
2482
2254
  */
2483
2255
  getProviderName() {
2484
- return "claude";
2256
+ return "kimi";
2485
2257
  }
2486
2258
  /**
2487
2259
  * Get the list of supported models
2488
- * @returns Array of supported model names
2489
2260
  */
2490
2261
  getSupportedModels() {
2491
- return [
2492
- MODEL_CLAUDE_3_HAIKU,
2493
- MODEL_CLAUDE_3_5_HAIKU,
2494
- MODEL_CLAUDE_3_5_SONNET,
2495
- MODEL_CLAUDE_3_7_SONNET,
2496
- MODEL_CLAUDE_4_SONNET,
2497
- MODEL_CLAUDE_4_OPUS,
2498
- MODEL_CLAUDE_4_5_SONNET,
2499
- MODEL_CLAUDE_4_5_HAIKU,
2500
- MODEL_CLAUDE_4_5_OPUS
2501
- ];
2262
+ return [MODEL_KIMI_K2_5];
2502
2263
  }
2503
2264
  /**
2504
2265
  * Get the default model
2505
- * @returns Default model name
2506
2266
  */
2507
2267
  getDefaultModel() {
2508
- return MODEL_CLAUDE_3_HAIKU;
2268
+ return MODEL_KIMI_K2_5;
2509
2269
  }
2510
2270
  /**
2511
- * Check if this provider supports vision (image processing)
2512
- * @returns Vision support status (true)
2271
+ * Get the default vision model
2272
+ */
2273
+ getDefaultVisionModel() {
2274
+ return MODEL_KIMI_K2_5;
2275
+ }
2276
+ /**
2277
+ * Check if this provider supports vision
2513
2278
  */
2514
2279
  supportsVision() {
2515
2280
  return true;
2516
2281
  }
2517
2282
  /**
2518
2283
  * Check if a specific model supports vision capabilities
2519
- * @param model The model name to check
2520
- * @returns True if the model supports vision, false otherwise
2521
2284
  */
2522
2285
  supportsVisionForModel(model) {
2523
- return CLAUDE_VISION_SUPPORTED_MODELS.includes(model);
2286
+ return isKimiVisionModel(model);
2287
+ }
2288
+ resolveEndpoint(options) {
2289
+ if (options.endpoint) {
2290
+ return this.normalizeEndpoint(options.endpoint);
2291
+ }
2292
+ if (options.baseUrl) {
2293
+ const baseUrl = this.normalizeEndpoint(options.baseUrl);
2294
+ if (baseUrl.endsWith("/chat/completions")) {
2295
+ return baseUrl;
2296
+ }
2297
+ return `${baseUrl}/chat/completions`;
2298
+ }
2299
+ return ENDPOINT_KIMI_CHAT_COMPLETIONS_API;
2300
+ }
2301
+ normalizeEndpoint(value) {
2302
+ return value.replace(/\/+$/, "");
2524
2303
  }
2525
2304
  };
2526
2305
 
2527
- // src/services/providers/openrouter/OpenRouterChatService.ts
2528
- var OpenRouterChatService = class {
2306
+ // src/services/providers/openai/OpenAIChatService.ts
2307
+ var OpenAIChatService = class {
2529
2308
  /**
2530
2309
  * Constructor
2531
- * @param apiKey OpenRouter API key
2310
+ * @param apiKey OpenAI API key
2532
2311
  * @param model Name of the model to use
2533
2312
  * @param visionModel Name of the vision model
2534
- * @param tools Tool definitions (optional)
2535
- * @param endpoint API endpoint (optional)
2536
- * @param responseLength Response length configuration (optional)
2537
- * @param appName Application name for OpenRouter analytics (optional)
2538
- * @param appUrl Application URL for OpenRouter analytics (optional)
2539
- * @param reasoning_effort Reasoning effort level (optional)
2540
- * @param includeReasoning Whether to include reasoning in response (optional)
2541
- * @param reasoningMaxTokens Maximum tokens for reasoning (optional)
2542
2313
  */
2543
- constructor(apiKey, model = MODEL_GPT_OSS_20B_FREE, visionModel = MODEL_GPT_OSS_20B_FREE, tools, endpoint = ENDPOINT_OPENROUTER_API, responseLength, appName, appUrl, reasoning_effort, includeReasoning, reasoningMaxTokens) {
2314
+ 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) {
2544
2315
  /** Provider name */
2545
- this.provider = "openrouter";
2546
- this.lastRequestTime = 0;
2547
- this.requestCount = 0;
2316
+ this.provider = "openai";
2548
2317
  this.apiKey = apiKey;
2549
2318
  this.model = model;
2550
2319
  this.tools = tools || [];
2551
2320
  this.endpoint = endpoint;
2321
+ this.mcpServers = mcpServers;
2552
2322
  this.responseLength = responseLength;
2553
- this.appName = appName;
2554
- this.appUrl = appUrl;
2323
+ this.verbosity = verbosity;
2555
2324
  this.reasoning_effort = reasoning_effort;
2556
- this.includeReasoning = includeReasoning;
2557
- this.reasoningMaxTokens = reasoningMaxTokens;
2325
+ this.enableReasoningSummary = enableReasoningSummary;
2326
+ if (!VISION_SUPPORTED_MODELS.includes(visionModel)) {
2327
+ throw new Error(
2328
+ `Model ${visionModel} does not support vision capabilities.`
2329
+ );
2330
+ }
2558
2331
  this.visionModel = visionModel;
2559
2332
  }
2560
2333
  /**
@@ -2571,31 +2344,6 @@ If it's in another language, summarize in that language.
2571
2344
  getVisionModel() {
2572
2345
  return this.visionModel;
2573
2346
  }
2574
- /**
2575
- * Apply rate limiting for free tier models
2576
- */
2577
- async applyRateLimiting() {
2578
- if (!isOpenRouterFreeModel(this.model)) {
2579
- return;
2580
- }
2581
- const now = Date.now();
2582
- const timeSinceLastRequest = now - this.lastRequestTime;
2583
- if (timeSinceLastRequest > 6e4) {
2584
- this.requestCount = 0;
2585
- }
2586
- if (this.requestCount >= OPENROUTER_FREE_RATE_LIMIT_PER_MINUTE) {
2587
- const waitTime = 6e4 - timeSinceLastRequest;
2588
- if (waitTime > 0) {
2589
- console.log(
2590
- `Rate limit reached for free tier. Waiting ${waitTime}ms...`
2591
- );
2592
- await new Promise((resolve) => setTimeout(resolve, waitTime));
2593
- this.requestCount = 0;
2594
- }
2595
- }
2596
- this.lastRequestTime = now;
2597
- this.requestCount++;
2598
- }
2599
2347
  /**
2600
2348
  * Process chat messages
2601
2349
  * @param messages Array of messages to send
@@ -2603,56 +2351,65 @@ If it's in another language, summarize in that language.
2603
2351
  * @param onCompleteResponse Callback to execute when response is complete
2604
2352
  */
2605
2353
  async processChat(messages, onPartialResponse, onCompleteResponse) {
2606
- await this.applyRateLimiting();
2607
- if (this.tools.length === 0) {
2608
- const res = await this.callOpenRouter(messages, this.model, true);
2609
- const full = await this.handleStream(res, onPartialResponse);
2610
- await onCompleteResponse(full);
2611
- return;
2612
- }
2613
- const { blocks, stop_reason } = await this.chatOnce(messages);
2614
- if (stop_reason === "end") {
2615
- const full = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2616
- await onCompleteResponse(full);
2617
- return;
2618
- }
2619
- throw new Error(
2620
- "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
2621
- );
2354
+ await processChatWithOptionalTools({
2355
+ hasTools: this.tools.length > 0,
2356
+ runWithoutTools: async () => {
2357
+ const res = await this.callOpenAI(messages, this.model, true);
2358
+ const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2359
+ try {
2360
+ if (isResponsesAPI) {
2361
+ const result = await this.parseResponsesStream(
2362
+ res,
2363
+ onPartialResponse
2364
+ );
2365
+ return StreamTextAccumulator.getFullText(result.blocks);
2366
+ }
2367
+ return this.handleStream(res, onPartialResponse);
2368
+ } catch (error) {
2369
+ console.error("[processChat] Error in streaming/completion:", error);
2370
+ throw error;
2371
+ }
2372
+ },
2373
+ runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
2374
+ onCompleteResponse,
2375
+ toolErrorMessage: "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
2376
+ });
2622
2377
  }
2623
2378
  /**
2624
2379
  * Process chat messages with images
2625
2380
  * @param messages Array of messages to send (including images)
2626
2381
  * @param onPartialResponse Callback to receive each part of streaming response
2627
2382
  * @param onCompleteResponse Callback to execute when response is complete
2383
+ * @throws Error if the selected model doesn't support vision
2628
2384
  */
2629
2385
  async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
2630
- if (!isOpenRouterVisionModel(this.visionModel)) {
2631
- throw new Error(
2632
- `Model ${this.visionModel} does not support vision capabilities.`
2633
- );
2634
- }
2635
- await this.applyRateLimiting();
2636
2386
  try {
2637
- if (this.tools.length === 0) {
2638
- const res = await this.callOpenRouter(messages, this.visionModel, true);
2639
- const full = await this.handleStream(res, onPartialResponse);
2640
- await onCompleteResponse(full);
2641
- return;
2642
- }
2643
- const { blocks, stop_reason } = await this.visionChatOnce(
2644
- messages,
2645
- true,
2646
- onPartialResponse
2647
- );
2648
- if (stop_reason === "end") {
2649
- const full = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2650
- await onCompleteResponse(full);
2651
- return;
2652
- }
2653
- throw new Error(
2654
- "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
2655
- );
2387
+ await processChatWithOptionalTools({
2388
+ hasTools: this.tools.length > 0,
2389
+ runWithoutTools: async () => {
2390
+ const res = await this.callOpenAI(messages, this.visionModel, true);
2391
+ const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2392
+ try {
2393
+ if (isResponsesAPI) {
2394
+ const result = await this.parseResponsesStream(
2395
+ res,
2396
+ onPartialResponse
2397
+ );
2398
+ return StreamTextAccumulator.getFullText(result.blocks);
2399
+ }
2400
+ return this.handleStream(res, onPartialResponse);
2401
+ } catch (streamError) {
2402
+ console.error(
2403
+ "[processVisionChat] Error in streaming/completion:",
2404
+ streamError
2405
+ );
2406
+ throw streamError;
2407
+ }
2408
+ },
2409
+ runWithTools: () => this.visionChatOnce(messages, true, onPartialResponse),
2410
+ onCompleteResponse,
2411
+ toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
2412
+ });
2656
2413
  } catch (error) {
2657
2414
  console.error("Error in processVisionChat:", error);
2658
2415
  throw error;
@@ -2668,14 +2425,8 @@ If it's in another language, summarize in that language.
2668
2425
  */
2669
2426
  async chatOnce(messages, stream = true, onPartialResponse = () => {
2670
2427
  }, maxTokens) {
2671
- await this.applyRateLimiting();
2672
- const res = await this.callOpenRouter(
2673
- messages,
2674
- this.model,
2675
- stream,
2676
- maxTokens
2677
- );
2678
- return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
2428
+ const res = await this.callOpenAI(messages, this.model, stream, maxTokens);
2429
+ return this.parseResponse(res, stream, onPartialResponse);
2679
2430
  }
2680
2431
  /**
2681
2432
  * Process vision chat messages with tools
@@ -2687,121 +2438,192 @@ If it's in another language, summarize in that language.
2687
2438
  */
2688
2439
  async visionChatOnce(messages, stream = false, onPartialResponse = () => {
2689
2440
  }, maxTokens) {
2690
- if (!isOpenRouterVisionModel(this.visionModel)) {
2691
- throw new Error(
2692
- `Model ${this.visionModel} does not support vision capabilities.`
2693
- );
2694
- }
2695
- await this.applyRateLimiting();
2696
- const res = await this.callOpenRouter(
2441
+ const res = await this.callOpenAI(
2697
2442
  messages,
2698
2443
  this.visionModel,
2699
2444
  stream,
2700
2445
  maxTokens
2701
2446
  );
2702
- return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
2447
+ return this.parseResponse(res, stream, onPartialResponse);
2703
2448
  }
2704
2449
  /**
2705
- * Call OpenRouter API
2450
+ * Parse response based on endpoint type
2706
2451
  */
2707
- async callOpenRouter(messages, model, stream = false, maxTokens) {
2708
- const body = this.buildRequestBody(messages, model, stream, maxTokens);
2709
- const headers = {
2710
- Authorization: `Bearer ${this.apiKey}`
2711
- };
2712
- if (this.appUrl) {
2713
- headers["HTTP-Referer"] = this.appUrl;
2714
- }
2715
- if (this.appName) {
2716
- headers["X-Title"] = this.appName;
2452
+ async parseResponse(res, stream, onPartialResponse) {
2453
+ const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2454
+ if (isResponsesAPI) {
2455
+ return stream ? this.parseResponsesStream(res, onPartialResponse) : this.parseResponsesOneShot(await res.json());
2717
2456
  }
2718
- const res = await ChatServiceHttpClient.post(this.endpoint, body, headers);
2457
+ return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
2458
+ }
2459
+ async callOpenAI(messages, model, stream = false, maxTokens) {
2460
+ const body = this.buildRequestBody(messages, model, stream, maxTokens);
2461
+ const res = await ChatServiceHttpClient.post(this.endpoint, body, {
2462
+ Authorization: `Bearer ${this.apiKey}`
2463
+ });
2719
2464
  return res;
2720
2465
  }
2721
2466
  /**
2722
- * Build request body for OpenRouter API (OpenAI-compatible format)
2467
+ * Build request body based on the endpoint type
2723
2468
  */
2724
2469
  buildRequestBody(messages, model, stream, maxTokens) {
2470
+ const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2471
+ this.validateMCPCompatibility();
2725
2472
  const body = {
2726
2473
  model,
2727
- messages,
2728
2474
  stream
2729
2475
  };
2730
2476
  const tokenLimit = maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength);
2731
- if (tokenLimit) {
2732
- console.warn(
2733
- `OpenRouter: Token limits are not supported for gpt-oss-20b model due to known issues. Using unlimited tokens instead.`
2734
- );
2477
+ if (isResponsesAPI) {
2478
+ body.max_output_tokens = tokenLimit;
2479
+ } else {
2480
+ body.max_completion_tokens = tokenLimit;
2735
2481
  }
2736
- if (this.reasoning_effort !== void 0 || this.includeReasoning !== void 0 || this.reasoningMaxTokens) {
2737
- body.reasoning = {};
2738
- if (this.reasoning_effort && this.reasoning_effort !== "none") {
2739
- const effort = this.reasoning_effort === "minimal" ? "low" : this.reasoning_effort;
2740
- body.reasoning.effort = effort;
2741
- }
2742
- if (this.reasoning_effort === "none" || this.includeReasoning !== true) {
2743
- body.reasoning.exclude = true;
2744
- }
2745
- if (this.reasoningMaxTokens) {
2746
- body.reasoning.max_tokens = this.reasoningMaxTokens;
2747
- }
2482
+ if (isResponsesAPI) {
2483
+ body.input = this.cleanMessagesForResponsesAPI(messages);
2748
2484
  } else {
2749
- body.reasoning = { exclude: true };
2485
+ body.messages = messages;
2750
2486
  }
2751
- if (this.tools.length > 0) {
2752
- body.tools = this.tools.map((t) => ({
2753
- type: "function",
2754
- function: {
2755
- name: t.name,
2756
- description: t.description,
2757
- parameters: t.parameters
2487
+ if (isGPT5Model(model)) {
2488
+ if (isResponsesAPI) {
2489
+ if (this.reasoning_effort) {
2490
+ body.reasoning = {
2491
+ ...body.reasoning,
2492
+ effort: this.reasoning_effort
2493
+ };
2494
+ if (this.enableReasoningSummary) {
2495
+ body.reasoning.summary = "auto";
2496
+ }
2758
2497
  }
2759
- }));
2760
- body.tool_choice = "auto";
2498
+ if (this.verbosity) {
2499
+ body.text = {
2500
+ ...body.text,
2501
+ format: { type: "text" },
2502
+ verbosity: this.verbosity
2503
+ };
2504
+ }
2505
+ } else {
2506
+ if (this.reasoning_effort) {
2507
+ body.reasoning_effort = this.reasoning_effort;
2508
+ }
2509
+ if (this.verbosity) {
2510
+ body.verbosity = this.verbosity;
2511
+ }
2512
+ }
2513
+ }
2514
+ const tools = this.buildToolsDefinition();
2515
+ if (tools.length > 0) {
2516
+ body.tools = tools;
2517
+ if (!isResponsesAPI) {
2518
+ body.tool_choice = "auto";
2519
+ }
2761
2520
  }
2762
2521
  return body;
2763
2522
  }
2764
2523
  /**
2765
- * Handle streaming response from OpenRouter
2766
- * OpenRouter uses SSE format with potential comment lines
2524
+ * Validate MCP servers compatibility with the current endpoint
2767
2525
  */
2768
- async handleStream(res, onPartial) {
2769
- const reader = res.body.getReader();
2770
- const dec = new TextDecoder();
2771
- let buffer = "";
2772
- let full = "";
2773
- while (true) {
2774
- const { done, value } = await reader.read();
2775
- if (done) break;
2776
- buffer += dec.decode(value, { stream: true });
2777
- const lines = buffer.split("\n");
2778
- buffer = lines.pop() || "";
2779
- for (const line of lines) {
2780
- const trimmedLine = line.trim();
2781
- if (!trimmedLine || trimmedLine.startsWith(":")) continue;
2782
- if (!trimmedLine.startsWith("data:")) continue;
2783
- const jsonStr = trimmedLine.slice(5).trim();
2784
- if (jsonStr === "[DONE]") {
2785
- return full;
2786
- }
2787
- try {
2788
- const json = JSON.parse(jsonStr);
2789
- const content = json.choices?.[0]?.delta?.content || "";
2790
- if (content) {
2791
- onPartial(content);
2792
- full += content;
2526
+ validateMCPCompatibility() {
2527
+ if (this.mcpServers.length > 0 && this.endpoint === ENDPOINT_OPENAI_CHAT_COMPLETIONS_API) {
2528
+ throw new Error(
2529
+ `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.`
2530
+ );
2531
+ }
2532
+ }
2533
+ /**
2534
+ * Clean messages for Responses API (remove timestamp and other extra properties)
2535
+ */
2536
+ cleanMessagesForResponsesAPI(messages) {
2537
+ return messages.map((msg) => {
2538
+ const role = msg.role === "tool" ? "user" : msg.role;
2539
+ const cleanMsg = {
2540
+ role
2541
+ };
2542
+ if (typeof msg.content === "string") {
2543
+ cleanMsg.content = msg.content;
2544
+ } else if (Array.isArray(msg.content)) {
2545
+ cleanMsg.content = msg.content.map((block) => {
2546
+ if (block.type === "text") {
2547
+ return {
2548
+ type: "input_text",
2549
+ text: block.text
2550
+ };
2551
+ } else if (block.type === "image_url") {
2552
+ return {
2553
+ type: "input_image",
2554
+ image_url: block.image_url.url
2555
+ // Extract the URL string directly
2556
+ };
2793
2557
  }
2794
- } catch (e) {
2795
- console.debug("Failed to parse SSE data:", jsonStr);
2796
- }
2558
+ return block;
2559
+ });
2560
+ } else {
2561
+ cleanMsg.content = msg.content;
2797
2562
  }
2563
+ return cleanMsg;
2564
+ });
2565
+ }
2566
+ /**
2567
+ * Build tools definition based on the endpoint type
2568
+ */
2569
+ buildToolsDefinition() {
2570
+ const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2571
+ const toolDefs = [];
2572
+ if (this.tools.length > 0) {
2573
+ toolDefs.push(
2574
+ ...buildOpenAICompatibleTools(
2575
+ this.tools,
2576
+ isResponsesAPI ? "responses" : "chat-completions"
2577
+ )
2578
+ );
2579
+ }
2580
+ if (this.mcpServers.length > 0 && isResponsesAPI) {
2581
+ toolDefs.push(...this.buildMCPToolsDefinition());
2798
2582
  }
2799
- return full;
2583
+ return toolDefs;
2800
2584
  }
2801
2585
  /**
2802
- * Parse streaming response with tool support
2586
+ * Build MCP tools definition for Responses API
2803
2587
  */
2588
+ buildMCPToolsDefinition() {
2589
+ return this.mcpServers.map((server) => {
2590
+ const mcpDef = {
2591
+ type: "mcp",
2592
+ // Using 'mcp' as indicated by the error message
2593
+ server_label: server.name,
2594
+ // Use server_label as required by API
2595
+ server_url: server.url
2596
+ // Use server_url instead of url
2597
+ };
2598
+ if (server.require_approval) {
2599
+ mcpDef.require_approval = server.require_approval;
2600
+ }
2601
+ if (server.tool_configuration?.allowed_tools) {
2602
+ mcpDef.allowed_tools = server.tool_configuration.allowed_tools;
2603
+ }
2604
+ if (server.authorization_token) {
2605
+ mcpDef.headers = {
2606
+ Authorization: `Bearer ${server.authorization_token}`
2607
+ };
2608
+ }
2609
+ return mcpDef;
2610
+ });
2611
+ }
2612
+ async handleStream(res, onPartial) {
2613
+ return parseOpenAICompatibleTextStream(res, onPartial);
2614
+ }
2804
2615
  async parseStream(res, onPartial) {
2616
+ return parseOpenAICompatibleToolStream(res, onPartial, {
2617
+ appendTextBlock: StreamTextAccumulator.addTextBlock
2618
+ });
2619
+ }
2620
+ parseOneShot(data) {
2621
+ return parseOpenAICompatibleOneShot(data);
2622
+ }
2623
+ /**
2624
+ * Parse streaming response from Responses API (SSE format)
2625
+ */
2626
+ async parseResponsesStream(res, onPartial) {
2805
2627
  const reader = res.body.getReader();
2806
2628
  const dec = new TextDecoder();
2807
2629
  const textBlocks = [];
@@ -2811,164 +2633,221 @@ If it's in another language, summarize in that language.
2811
2633
  const { done, value } = await reader.read();
2812
2634
  if (done) break;
2813
2635
  buf += dec.decode(value, { stream: true });
2636
+ let eventType = "";
2637
+ let eventData = "";
2814
2638
  const lines = buf.split("\n");
2815
2639
  buf = lines.pop() || "";
2816
- for (const line of lines) {
2817
- const trimmedLine = line.trim();
2818
- if (!trimmedLine || trimmedLine.startsWith(":")) continue;
2819
- if (!trimmedLine.startsWith("data:")) continue;
2820
- const payload = trimmedLine.slice(5).trim();
2821
- if (payload === "[DONE]") {
2822
- break;
2823
- }
2824
- try {
2825
- const json = JSON.parse(payload);
2826
- const delta = json.choices?.[0]?.delta;
2827
- if (delta?.content) {
2828
- onPartial(delta.content);
2829
- StreamTextAccumulator.append(textBlocks, delta.content);
2830
- }
2831
- if (delta?.tool_calls) {
2832
- delta.tool_calls.forEach((c) => {
2833
- const entry = toolCallsMap.get(c.index) ?? {
2834
- id: c.id,
2835
- name: c.function?.name,
2836
- args: ""
2837
- };
2838
- entry.args += c.function?.arguments || "";
2839
- toolCallsMap.set(c.index, entry);
2840
- });
2640
+ for (let i = 0; i < lines.length; i++) {
2641
+ const line = lines[i].trim();
2642
+ if (line.startsWith("event:")) {
2643
+ eventType = line.slice(6).trim();
2644
+ } else if (line.startsWith("data:")) {
2645
+ eventData = line.slice(5).trim();
2646
+ } else if (line === "" && eventType && eventData) {
2647
+ try {
2648
+ const json = JSON.parse(eventData);
2649
+ const completionResult = this.handleResponsesSSEEvent(
2650
+ eventType,
2651
+ json,
2652
+ onPartial,
2653
+ textBlocks,
2654
+ toolCallsMap
2655
+ );
2656
+ if (completionResult === "completed") {
2657
+ }
2658
+ } catch (e) {
2659
+ console.warn("Failed to parse SSE data:", eventData);
2841
2660
  }
2842
- } catch (e) {
2843
- console.debug("Failed to parse SSE data:", payload);
2661
+ eventType = "";
2662
+ eventData = "";
2844
2663
  }
2845
2664
  }
2846
2665
  }
2847
- const toolBlocks = Array.from(toolCallsMap.entries()).sort((a, b) => a[0] - b[0]).map(([_, e]) => ({
2848
- type: "tool_use",
2849
- id: e.id,
2850
- name: e.name,
2851
- input: JSON.parse(e.args || "{}")
2852
- }));
2853
- const blocks = [...textBlocks, ...toolBlocks];
2854
- return {
2855
- blocks,
2856
- stop_reason: toolBlocks.length ? "tool_use" : "end"
2857
- };
2666
+ const toolBlocks = Array.from(toolCallsMap.values()).map(
2667
+ (tool) => ({
2668
+ type: "tool_use",
2669
+ id: tool.id,
2670
+ name: tool.name,
2671
+ input: tool.input || {}
2672
+ })
2673
+ );
2674
+ const blocks = [...textBlocks, ...toolBlocks];
2675
+ return {
2676
+ blocks,
2677
+ stop_reason: toolBlocks.length ? "tool_use" : "end"
2678
+ };
2679
+ }
2680
+ /**
2681
+ * Handle specific SSE events from Responses API
2682
+ * @returns 'completed' if the response is completed, undefined otherwise
2683
+ */
2684
+ handleResponsesSSEEvent(eventType, data, onPartial, textBlocks, toolCallsMap) {
2685
+ switch (eventType) {
2686
+ // Item addition events
2687
+ case "response.output_item.added":
2688
+ if (data.item?.type === "message" && Array.isArray(data.item.content)) {
2689
+ data.item.content.forEach((c) => {
2690
+ if (c.type === "output_text" && c.text) {
2691
+ onPartial(c.text);
2692
+ StreamTextAccumulator.append(textBlocks, c.text);
2693
+ }
2694
+ });
2695
+ } else if (data.item?.type === "function_call") {
2696
+ toolCallsMap.set(data.item.id, {
2697
+ id: data.item.id,
2698
+ name: data.item.name,
2699
+ input: data.item.arguments ? JSON.parse(data.item.arguments) : {}
2700
+ });
2701
+ }
2702
+ break;
2703
+ // Initial content part events
2704
+ case "response.content_part.added":
2705
+ if (data.part?.type === "output_text" && typeof data.part.text === "string") {
2706
+ onPartial(data.part.text);
2707
+ StreamTextAccumulator.append(textBlocks, data.part.text);
2708
+ }
2709
+ break;
2710
+ // Text delta events
2711
+ case "response.output_text.delta":
2712
+ case "response.content_part.delta":
2713
+ {
2714
+ const deltaText = typeof data.delta === "string" ? data.delta : data.delta?.text ?? "";
2715
+ if (deltaText) {
2716
+ onPartial(deltaText);
2717
+ StreamTextAccumulator.append(textBlocks, deltaText);
2718
+ }
2719
+ }
2720
+ break;
2721
+ // Text completion events - do not add text here as it's already accumulated via delta events
2722
+ case "response.output_text.done":
2723
+ case "response.content_part.done":
2724
+ break;
2725
+ // Response completion events
2726
+ case "response.completed":
2727
+ return "completed";
2728
+ // GPT-5 reasoning token events (not visible but counted for billing)
2729
+ case "response.reasoning.started":
2730
+ case "response.reasoning.delta":
2731
+ case "response.reasoning.done":
2732
+ break;
2733
+ default:
2734
+ break;
2735
+ }
2736
+ return void 0;
2858
2737
  }
2859
2738
  /**
2860
- * Parse non-streaming response
2739
+ * Parse non-streaming response from Responses API
2861
2740
  */
2862
- parseOneShot(data) {
2863
- const choice = data.choices?.[0];
2741
+ parseResponsesOneShot(data) {
2864
2742
  const blocks = [];
2865
- if (choice?.finish_reason === "tool_calls" && choice?.message?.tool_calls) {
2866
- choice.message.tool_calls.forEach(
2867
- (c) => blocks.push({
2868
- type: "tool_use",
2869
- id: c.id,
2870
- name: c.function?.name,
2871
- input: JSON.parse(c.function?.arguments || "{}")
2872
- })
2873
- );
2874
- } else if (choice?.message?.content) {
2875
- blocks.push({ type: "text", text: choice.message.content });
2743
+ if (data.output && Array.isArray(data.output)) {
2744
+ data.output.forEach((outputItem) => {
2745
+ if (outputItem.type === "message" && outputItem.content) {
2746
+ outputItem.content.forEach((content) => {
2747
+ if (content.type === "output_text" && content.text) {
2748
+ blocks.push({ type: "text", text: content.text });
2749
+ }
2750
+ });
2751
+ }
2752
+ if (outputItem.type === "function_call") {
2753
+ blocks.push({
2754
+ type: "tool_use",
2755
+ id: outputItem.id,
2756
+ name: outputItem.name,
2757
+ input: outputItem.arguments ? JSON.parse(outputItem.arguments) : {}
2758
+ });
2759
+ }
2760
+ });
2876
2761
  }
2877
2762
  return {
2878
2763
  blocks,
2879
- stop_reason: choice?.finish_reason === "tool_calls" ? "tool_use" : "end"
2764
+ stop_reason: blocks.some((b) => b.type === "tool_use") ? "tool_use" : "end"
2880
2765
  };
2881
2766
  }
2882
2767
  };
2883
2768
 
2884
- // src/services/providers/openrouter/OpenRouterChatServiceProvider.ts
2885
- var OpenRouterChatServiceProvider = class {
2769
+ // src/services/providers/openai/OpenAIChatServiceProvider.ts
2770
+ var OpenAIChatServiceProvider = class {
2886
2771
  /**
2887
2772
  * Create a chat service instance
2888
2773
  * @param options Service options
2889
- * @returns OpenRouterChatService instance
2774
+ * @returns OpenAIChatService instance
2890
2775
  */
2891
2776
  createChatService(options) {
2892
- const visionModel = options.visionModel || options.model || this.getDefaultModel();
2893
- if (options.visionModel && !this.supportsVisionForModel(options.visionModel)) {
2894
- throw new Error(
2895
- `Model ${options.visionModel} does not support vision capabilities.`
2896
- );
2777
+ const optimizedOptions = this.optimizeGPT5Options(options);
2778
+ const visionModel = resolveVisionModel({
2779
+ model: optimizedOptions.model,
2780
+ visionModel: optimizedOptions.visionModel,
2781
+ defaultModel: this.getDefaultModel(),
2782
+ defaultVisionModel: this.getDefaultModel(),
2783
+ supportsVisionForModel: (model) => this.supportsVisionForModel(model),
2784
+ validate: "resolved"
2785
+ });
2786
+ const tools = optimizedOptions.tools;
2787
+ const mcpServers = optimizedOptions.mcpServers ?? [];
2788
+ const modelName = optimizedOptions.model || this.getDefaultModel();
2789
+ let shouldUseResponsesAPI = false;
2790
+ if (mcpServers.length > 0) {
2791
+ shouldUseResponsesAPI = true;
2792
+ } else if (isGPT5Model(modelName)) {
2793
+ const preference = optimizedOptions.gpt5EndpointPreference || "chat";
2794
+ shouldUseResponsesAPI = preference === "responses";
2897
2795
  }
2898
- const tools = options.tools;
2899
- const appName = options.appName;
2900
- const appUrl = options.appUrl;
2901
- return new OpenRouterChatService(
2902
- options.apiKey,
2903
- options.model || this.getDefaultModel(),
2796
+ const endpoint = optimizedOptions.endpoint || (shouldUseResponsesAPI ? ENDPOINT_OPENAI_RESPONSES_API : ENDPOINT_OPENAI_CHAT_COMPLETIONS_API);
2797
+ return new OpenAIChatService(
2798
+ optimizedOptions.apiKey,
2799
+ modelName,
2904
2800
  visionModel,
2905
2801
  tools,
2906
- options.endpoint,
2907
- options.responseLength,
2908
- appName,
2909
- appUrl,
2910
- options.reasoning_effort,
2911
- options.includeReasoning,
2912
- options.reasoningMaxTokens
2802
+ endpoint,
2803
+ mcpServers,
2804
+ optimizedOptions.responseLength,
2805
+ optimizedOptions.verbosity,
2806
+ optimizedOptions.reasoning_effort,
2807
+ optimizedOptions.enableReasoningSummary
2913
2808
  );
2914
2809
  }
2915
2810
  /**
2916
2811
  * Get the provider name
2917
- * @returns Provider name ('openrouter')
2812
+ * @returns Provider name ('openai')
2918
2813
  */
2919
2814
  getProviderName() {
2920
- return "openrouter";
2815
+ return "openai";
2921
2816
  }
2922
2817
  /**
2923
2818
  * Get the list of supported models
2924
- * Supports a curated list of OpenRouter models
2925
2819
  * @returns Array of supported model names
2926
2820
  */
2927
2821
  getSupportedModels() {
2928
2822
  return [
2929
- // Free models
2930
- MODEL_GPT_OSS_20B_FREE,
2931
- MODEL_ZAI_GLM_4_5_AIR_FREE,
2932
- // OpenAI models
2933
- MODEL_OPENAI_GPT_5_1_CHAT,
2934
- MODEL_OPENAI_GPT_5_1_CODEX,
2935
- MODEL_OPENAI_GPT_5_MINI,
2936
- MODEL_OPENAI_GPT_5_NANO,
2937
- MODEL_OPENAI_GPT_4O,
2938
- MODEL_OPENAI_GPT_4_1_MINI,
2939
- MODEL_OPENAI_GPT_4_1_NANO,
2940
- // Anthropic models
2941
- MODEL_ANTHROPIC_CLAUDE_OPUS_4,
2942
- MODEL_ANTHROPIC_CLAUDE_SONNET_4,
2943
- MODEL_ANTHROPIC_CLAUDE_3_7_SONNET,
2944
- MODEL_ANTHROPIC_CLAUDE_3_5_SONNET,
2945
- MODEL_ANTHROPIC_CLAUDE_4_5_HAIKU,
2946
- // Gemini models
2947
- MODEL_GOOGLE_GEMINI_2_5_PRO,
2948
- MODEL_GOOGLE_GEMINI_2_5_FLASH,
2949
- MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE_PREVIEW_09_2025,
2950
- // Z.ai models
2951
- MODEL_ZAI_GLM_4_7_FLASH,
2952
- MODEL_ZAI_GLM_4_5_AIR,
2953
- // Other models
2954
- MODEL_MOONSHOTAI_KIMI_K2_5
2823
+ MODEL_GPT_5_NANO,
2824
+ MODEL_GPT_5_MINI,
2825
+ MODEL_GPT_5,
2826
+ MODEL_GPT_5_1,
2827
+ MODEL_GPT_4_1,
2828
+ MODEL_GPT_4_1_MINI,
2829
+ MODEL_GPT_4_1_NANO,
2830
+ MODEL_GPT_4O_MINI,
2831
+ MODEL_GPT_4O,
2832
+ MODEL_O3_MINI,
2833
+ MODEL_O1_MINI,
2834
+ MODEL_O1,
2835
+ MODEL_GPT_4_5_PREVIEW
2955
2836
  ];
2956
2837
  }
2957
2838
  /**
2958
2839
  * Get the default model
2959
- * @returns Default model name (gpt-oss-20b:free)
2840
+ * @returns Default model name
2960
2841
  */
2961
2842
  getDefaultModel() {
2962
- return MODEL_GPT_OSS_20B_FREE;
2843
+ return MODEL_GPT_5_NANO;
2963
2844
  }
2964
2845
  /**
2965
2846
  * Check if this provider supports vision (image processing)
2966
- * @returns Vision support status (false - gpt-oss-20b does not support vision)
2847
+ * @returns Vision support status (true)
2967
2848
  */
2968
2849
  supportsVision() {
2969
- return this.getSupportedModels().some(
2970
- (model) => this.supportsVisionForModel(model)
2971
- );
2850
+ return true;
2972
2851
  }
2973
2852
  /**
2974
2853
  * Check if a specific model supports vision capabilities
@@ -2976,388 +2855,437 @@ If it's in another language, summarize in that language.
2976
2855
  * @returns True if the model supports vision, false otherwise
2977
2856
  */
2978
2857
  supportsVisionForModel(model) {
2979
- return isOpenRouterVisionModel(model);
2858
+ return VISION_SUPPORTED_MODELS.includes(model);
2980
2859
  }
2981
2860
  /**
2982
- * Get list of free tier models
2983
- * @returns Array of free model names
2861
+ * Apply GPT-5 specific optimizations to options
2862
+ * @param options Original chat service options
2863
+ * @returns Optimized options for GPT-5 usage
2984
2864
  */
2985
- getFreeModels() {
2986
- return OPENROUTER_FREE_MODELS;
2865
+ optimizeGPT5Options(options) {
2866
+ const modelName = options.model || this.getDefaultModel();
2867
+ if (!isGPT5Model(modelName)) {
2868
+ return options;
2869
+ }
2870
+ const optimized = { ...options };
2871
+ if (options.gpt5Preset) {
2872
+ const preset = GPT5_PRESETS[options.gpt5Preset];
2873
+ optimized.reasoning_effort = preset.reasoning_effort;
2874
+ optimized.verbosity = preset.verbosity;
2875
+ } else {
2876
+ if (!options.reasoning_effort) {
2877
+ optimized.reasoning_effort = this.getDefaultReasoningEffortForModel(modelName);
2878
+ }
2879
+ }
2880
+ optimized.reasoning_effort = this.normalizeReasoningEffort(
2881
+ modelName,
2882
+ optimized.reasoning_effort
2883
+ );
2884
+ return optimized;
2987
2885
  }
2988
2886
  /**
2989
- * Check if a model is free tier
2990
- * @param model Model name to check
2991
- * @returns True if the model is free
2887
+ * Determine the default reasoning effort for GPT-5 family models
2888
+ * GPT-5.1 defaults to 'none' (fastest), earlier GPT-5 defaults to 'medium'
2992
2889
  */
2993
- isModelFree(model) {
2994
- return OPENROUTER_FREE_MODELS.includes(model) || model.endsWith(":free");
2890
+ getDefaultReasoningEffortForModel(modelName) {
2891
+ if (modelName === MODEL_GPT_5_1) {
2892
+ return "none";
2893
+ }
2894
+ return "medium";
2895
+ }
2896
+ normalizeReasoningEffort(modelName, effort) {
2897
+ if (!effort) {
2898
+ return void 0;
2899
+ }
2900
+ if (effort === "none" && !allowsReasoningNone(modelName)) {
2901
+ return this.getDefaultReasoningEffortForModel(modelName);
2902
+ }
2903
+ if (effort === "minimal" && !allowsReasoningMinimal(modelName)) {
2904
+ return "none";
2905
+ }
2906
+ return effort;
2995
2907
  }
2996
2908
  };
2997
2909
 
2998
- // src/services/providers/zai/ZAIChatService.ts
2999
- var ZAIChatService = class {
2910
+ // src/services/providers/openrouter/OpenRouterChatService.ts
2911
+ var OpenRouterChatService = class {
3000
2912
  /**
3001
2913
  * Constructor
3002
- * @param apiKey Z.ai API key
2914
+ * @param apiKey OpenRouter API key
3003
2915
  * @param model Name of the model to use
3004
2916
  * @param visionModel Name of the vision model
2917
+ * @param tools Tool definitions (optional)
2918
+ * @param endpoint API endpoint (optional)
2919
+ * @param responseLength Response length configuration (optional)
2920
+ * @param appName Application name for OpenRouter analytics (optional)
2921
+ * @param appUrl Application URL for OpenRouter analytics (optional)
2922
+ * @param reasoning_effort Reasoning effort level (optional)
2923
+ * @param includeReasoning Whether to include reasoning in response (optional)
2924
+ * @param reasoningMaxTokens Maximum tokens for reasoning (optional)
3005
2925
  */
3006
- constructor(apiKey, model = MODEL_GLM_4_7, visionModel = MODEL_GLM_4_6V_FLASH, tools, endpoint = ENDPOINT_ZAI_CHAT_COMPLETIONS_API, responseLength, responseFormat, thinking) {
2926
+ constructor(apiKey, model = MODEL_GPT_OSS_20B_FREE, visionModel = MODEL_GPT_OSS_20B_FREE, tools, endpoint = ENDPOINT_OPENROUTER_API, responseLength, appName, appUrl, reasoning_effort, includeReasoning, reasoningMaxTokens) {
3007
2927
  /** Provider name */
3008
- this.provider = "zai";
2928
+ this.provider = "openrouter";
2929
+ this.lastRequestTime = 0;
2930
+ this.requestCount = 0;
3009
2931
  this.apiKey = apiKey;
3010
2932
  this.model = model;
3011
2933
  this.tools = tools || [];
3012
2934
  this.endpoint = endpoint;
3013
2935
  this.responseLength = responseLength;
3014
- this.responseFormat = responseFormat;
3015
- this.thinking = thinking ?? { type: "disabled" };
2936
+ this.appName = appName;
2937
+ this.appUrl = appUrl;
2938
+ this.reasoning_effort = reasoning_effort;
2939
+ this.includeReasoning = includeReasoning;
2940
+ this.reasoningMaxTokens = reasoningMaxTokens;
3016
2941
  this.visionModel = visionModel;
3017
2942
  }
3018
2943
  /**
3019
2944
  * Get the current model name
2945
+ * @returns Model name
3020
2946
  */
3021
2947
  getModel() {
3022
2948
  return this.model;
3023
2949
  }
3024
2950
  /**
3025
2951
  * Get the current vision model name
2952
+ * @returns Vision model name
3026
2953
  */
3027
2954
  getVisionModel() {
3028
2955
  return this.visionModel;
3029
2956
  }
3030
2957
  /**
3031
- * Process chat messages
2958
+ * Apply rate limiting for free tier models
3032
2959
  */
3033
- async processChat(messages, onPartialResponse, onCompleteResponse) {
3034
- if (this.tools.length === 0) {
3035
- const res = await this.callZAI(messages, this.model, true);
3036
- const full = await this.handleStream(res, onPartialResponse);
3037
- await onCompleteResponse(full);
2960
+ async applyRateLimiting() {
2961
+ if (!isOpenRouterFreeModel(this.model)) {
3038
2962
  return;
3039
2963
  }
3040
- const { blocks, stop_reason } = await this.chatOnce(messages);
3041
- if (stop_reason === "end") {
3042
- const full = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
3043
- await onCompleteResponse(full);
3044
- return;
2964
+ const now = Date.now();
2965
+ const timeSinceLastRequest = now - this.lastRequestTime;
2966
+ if (timeSinceLastRequest > 6e4) {
2967
+ this.requestCount = 0;
3045
2968
  }
3046
- throw new Error(
3047
- "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
3048
- );
2969
+ if (this.requestCount >= OPENROUTER_FREE_RATE_LIMIT_PER_MINUTE) {
2970
+ const waitTime = 6e4 - timeSinceLastRequest;
2971
+ if (waitTime > 0) {
2972
+ console.log(
2973
+ `Rate limit reached for free tier. Waiting ${waitTime}ms...`
2974
+ );
2975
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
2976
+ this.requestCount = 0;
2977
+ }
2978
+ }
2979
+ this.lastRequestTime = now;
2980
+ this.requestCount++;
2981
+ }
2982
+ /**
2983
+ * Process chat messages
2984
+ * @param messages Array of messages to send
2985
+ * @param onPartialResponse Callback to receive each part of streaming response
2986
+ * @param onCompleteResponse Callback to execute when response is complete
2987
+ */
2988
+ async processChat(messages, onPartialResponse, onCompleteResponse) {
2989
+ await this.applyRateLimiting();
2990
+ await processChatWithOptionalTools({
2991
+ hasTools: this.tools.length > 0,
2992
+ runWithoutTools: async () => {
2993
+ const res = await this.callOpenRouter(messages, this.model, true);
2994
+ return this.handleStream(res, onPartialResponse);
2995
+ },
2996
+ runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
2997
+ onCompleteResponse,
2998
+ toolErrorMessage: "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
2999
+ });
3049
3000
  }
3050
3001
  /**
3051
3002
  * Process chat messages with images
3003
+ * @param messages Array of messages to send (including images)
3004
+ * @param onPartialResponse Callback to receive each part of streaming response
3005
+ * @param onCompleteResponse Callback to execute when response is complete
3052
3006
  */
3053
3007
  async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
3054
- if (!isZaiVisionModel(this.visionModel)) {
3008
+ if (!isOpenRouterVisionModel(this.visionModel)) {
3055
3009
  throw new Error(
3056
3010
  `Model ${this.visionModel} does not support vision capabilities.`
3057
3011
  );
3058
3012
  }
3059
- if (this.tools.length === 0) {
3060
- const res = await this.callZAI(messages, this.visionModel, true);
3061
- const full = await this.handleStream(res, onPartialResponse);
3062
- await onCompleteResponse(full);
3063
- return;
3064
- }
3065
- const { blocks, stop_reason } = await this.visionChatOnce(
3066
- messages,
3067
- true,
3068
- onPartialResponse
3069
- );
3070
- if (stop_reason === "end") {
3071
- const full = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
3072
- await onCompleteResponse(full);
3073
- return;
3013
+ await this.applyRateLimiting();
3014
+ try {
3015
+ await processChatWithOptionalTools({
3016
+ hasTools: this.tools.length > 0,
3017
+ runWithoutTools: async () => {
3018
+ const res = await this.callOpenRouter(
3019
+ messages,
3020
+ this.visionModel,
3021
+ true
3022
+ );
3023
+ return this.handleStream(res, onPartialResponse);
3024
+ },
3025
+ runWithTools: () => this.visionChatOnce(messages, true, onPartialResponse),
3026
+ onCompleteResponse,
3027
+ toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
3028
+ });
3029
+ } catch (error) {
3030
+ console.error("Error in processVisionChat:", error);
3031
+ throw error;
3074
3032
  }
3075
- throw new Error(
3076
- "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
3077
- );
3078
3033
  }
3079
3034
  /**
3080
3035
  * Process chat messages with tools (text only)
3036
+ * @param messages Array of messages to send
3037
+ * @param stream Whether to use streaming
3038
+ * @param onPartialResponse Callback for partial responses
3039
+ * @param maxTokens Maximum tokens for response (optional)
3040
+ * @returns Tool chat completion
3081
3041
  */
3082
3042
  async chatOnce(messages, stream = true, onPartialResponse = () => {
3083
3043
  }, maxTokens) {
3084
- const res = await this.callZAI(messages, this.model, stream, maxTokens);
3085
- return this.parseResponse(res, stream, onPartialResponse);
3044
+ await this.applyRateLimiting();
3045
+ const res = await this.callOpenRouter(
3046
+ messages,
3047
+ this.model,
3048
+ stream,
3049
+ maxTokens
3050
+ );
3051
+ return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
3086
3052
  }
3087
3053
  /**
3088
3054
  * Process vision chat messages with tools
3055
+ * @param messages Array of messages to send (including images)
3056
+ * @param stream Whether to use streaming
3057
+ * @param onPartialResponse Callback for partial responses
3058
+ * @param maxTokens Maximum tokens for response (optional)
3059
+ * @returns Tool chat completion
3089
3060
  */
3090
3061
  async visionChatOnce(messages, stream = false, onPartialResponse = () => {
3091
3062
  }, maxTokens) {
3092
- if (!isZaiVisionModel(this.visionModel)) {
3063
+ if (!isOpenRouterVisionModel(this.visionModel)) {
3093
3064
  throw new Error(
3094
3065
  `Model ${this.visionModel} does not support vision capabilities.`
3095
3066
  );
3096
3067
  }
3097
- const res = await this.callZAI(
3068
+ await this.applyRateLimiting();
3069
+ const res = await this.callOpenRouter(
3098
3070
  messages,
3099
3071
  this.visionModel,
3100
3072
  stream,
3101
3073
  maxTokens
3102
3074
  );
3103
- return this.parseResponse(res, stream, onPartialResponse);
3104
- }
3105
- async parseResponse(res, stream, onPartialResponse) {
3106
3075
  return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
3107
3076
  }
3108
- async callZAI(messages, model, stream = false, maxTokens) {
3077
+ /**
3078
+ * Call OpenRouter API
3079
+ */
3080
+ async callOpenRouter(messages, model, stream = false, maxTokens) {
3109
3081
  const body = this.buildRequestBody(messages, model, stream, maxTokens);
3110
- const res = await ChatServiceHttpClient.post(this.endpoint, body, {
3082
+ const headers = {
3111
3083
  Authorization: `Bearer ${this.apiKey}`
3112
- });
3084
+ };
3085
+ if (this.appUrl) {
3086
+ headers["HTTP-Referer"] = this.appUrl;
3087
+ }
3088
+ if (this.appName) {
3089
+ headers["X-Title"] = this.appName;
3090
+ }
3091
+ const res = await ChatServiceHttpClient.post(this.endpoint, body, headers);
3113
3092
  return res;
3114
3093
  }
3115
3094
  /**
3116
- * Build request body (OpenAI-compatible Chat Completions)
3095
+ * Build request body for OpenRouter API (OpenAI-compatible format)
3117
3096
  */
3118
3097
  buildRequestBody(messages, model, stream, maxTokens) {
3119
3098
  const body = {
3120
3099
  model,
3121
- stream,
3122
- messages
3100
+ messages,
3101
+ stream
3123
3102
  };
3124
3103
  const tokenLimit = maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength);
3125
- if (tokenLimit !== void 0) {
3126
- body.max_tokens = tokenLimit;
3127
- }
3128
- if (this.responseFormat) {
3129
- body.response_format = this.responseFormat;
3104
+ if (tokenLimit) {
3105
+ console.warn(
3106
+ `OpenRouter: Token limits are not supported for gpt-oss-20b model due to known issues. Using unlimited tokens instead.`
3107
+ );
3130
3108
  }
3131
- if (this.thinking) {
3132
- body.thinking = this.thinking;
3109
+ if (this.reasoning_effort !== void 0 || this.includeReasoning !== void 0 || this.reasoningMaxTokens) {
3110
+ body.reasoning = {};
3111
+ if (this.reasoning_effort && this.reasoning_effort !== "none") {
3112
+ const effort = this.reasoning_effort === "minimal" ? "low" : this.reasoning_effort;
3113
+ body.reasoning.effort = effort;
3114
+ }
3115
+ if (this.reasoning_effort === "none" || this.includeReasoning !== true) {
3116
+ body.reasoning.exclude = true;
3117
+ }
3118
+ if (this.reasoningMaxTokens) {
3119
+ body.reasoning.max_tokens = this.reasoningMaxTokens;
3120
+ }
3121
+ } else {
3122
+ body.reasoning = { exclude: true };
3133
3123
  }
3134
- const tools = this.buildToolsDefinition();
3135
- if (tools.length > 0) {
3136
- body.tools = tools;
3124
+ if (this.tools.length > 0) {
3125
+ body.tools = buildOpenAICompatibleTools(this.tools, "chat-completions");
3137
3126
  body.tool_choice = "auto";
3138
- if (stream && isZaiToolStreamModel(model)) {
3139
- body.tool_stream = true;
3140
- }
3141
3127
  }
3142
3128
  return body;
3143
3129
  }
3144
- buildToolsDefinition() {
3145
- if (this.tools.length === 0) return [];
3146
- return this.tools.map((t) => ({
3147
- type: "function",
3148
- function: {
3149
- name: t.name,
3150
- description: t.description,
3151
- parameters: t.parameters
3152
- }
3153
- }));
3154
- }
3130
+ /**
3131
+ * Handle streaming response from OpenRouter
3132
+ * OpenRouter uses SSE format with potential comment lines
3133
+ */
3155
3134
  async handleStream(res, onPartial) {
3156
- const reader = res.body.getReader();
3157
- const dec = new TextDecoder();
3158
- let full = "";
3159
- let buf = "";
3160
- while (true) {
3161
- const { done, value } = await reader.read();
3162
- if (done) break;
3163
- buf += dec.decode(value, { stream: true });
3164
- const lines = buf.split("\n");
3165
- buf = lines.pop() || "";
3166
- for (const line of lines) {
3167
- const trimmedLine = line.trim();
3168
- if (!trimmedLine || trimmedLine.startsWith(":")) continue;
3169
- if (!trimmedLine.startsWith("data:")) continue;
3170
- const payload = trimmedLine.slice(5).trim();
3171
- if (payload === "[DONE]") {
3172
- break;
3173
- }
3174
- try {
3175
- const json = JSON.parse(payload);
3176
- const content = json.choices?.[0]?.delta?.content || "";
3177
- if (content) {
3178
- onPartial(content);
3179
- full += content;
3180
- }
3181
- } catch (e) {
3182
- console.debug("Failed to parse SSE data:", payload);
3183
- }
3184
- }
3185
- }
3186
- return full;
3135
+ return parseOpenAICompatibleTextStream(res, onPartial, {
3136
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3137
+ });
3187
3138
  }
3188
3139
  /**
3189
3140
  * Parse streaming response with tool support
3190
3141
  */
3191
3142
  async parseStream(res, onPartial) {
3192
- const reader = res.body.getReader();
3193
- const dec = new TextDecoder();
3194
- const textBlocks = [];
3195
- const toolCallsMap = /* @__PURE__ */ new Map();
3196
- let buf = "";
3197
- while (true) {
3198
- const { done, value } = await reader.read();
3199
- if (done) break;
3200
- buf += dec.decode(value, { stream: true });
3201
- const lines = buf.split("\n");
3202
- buf = lines.pop() || "";
3203
- for (const line of lines) {
3204
- const trimmedLine = line.trim();
3205
- if (!trimmedLine || trimmedLine.startsWith(":")) continue;
3206
- if (!trimmedLine.startsWith("data:")) continue;
3207
- const payload = trimmedLine.slice(5).trim();
3208
- if (payload === "[DONE]") {
3209
- break;
3210
- }
3211
- try {
3212
- const json = JSON.parse(payload);
3213
- const delta = json.choices?.[0]?.delta;
3214
- if (delta?.content) {
3215
- onPartial(delta.content);
3216
- StreamTextAccumulator.append(textBlocks, delta.content);
3217
- }
3218
- if (delta?.tool_calls) {
3219
- delta.tool_calls.forEach((c) => {
3220
- const entry = toolCallsMap.get(c.index) ?? {
3221
- id: c.id,
3222
- name: c.function?.name,
3223
- args: ""
3224
- };
3225
- entry.args += c.function?.arguments || "";
3226
- toolCallsMap.set(c.index, entry);
3227
- });
3228
- }
3229
- } catch (e) {
3230
- console.debug("Failed to parse SSE data:", payload);
3231
- }
3232
- }
3233
- }
3234
- const toolBlocks = Array.from(toolCallsMap.entries()).sort((a, b) => a[0] - b[0]).map(([_, e]) => ({
3235
- type: "tool_use",
3236
- id: e.id,
3237
- name: e.name,
3238
- input: JSON.parse(e.args || "{}")
3239
- }));
3240
- const blocks = [...textBlocks, ...toolBlocks];
3241
- return {
3242
- blocks,
3243
- stop_reason: toolBlocks.length ? "tool_use" : "end"
3244
- };
3143
+ return parseOpenAICompatibleToolStream(res, onPartial, {
3144
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3145
+ });
3245
3146
  }
3246
3147
  /**
3247
3148
  * Parse non-streaming response
3248
3149
  */
3249
3150
  parseOneShot(data) {
3250
- const choice = data.choices?.[0];
3251
- const blocks = [];
3252
- if (choice?.message?.tool_calls?.length) {
3253
- choice.message.tool_calls.forEach(
3254
- (c) => blocks.push({
3255
- type: "tool_use",
3256
- id: c.id,
3257
- name: c.function?.name,
3258
- input: JSON.parse(c.function?.arguments || "{}")
3259
- })
3260
- );
3261
- } else if (choice?.message?.content) {
3262
- blocks.push({ type: "text", text: choice.message.content });
3263
- }
3264
- return {
3265
- blocks,
3266
- stop_reason: blocks.some((b) => b.type === "tool_use") ? "tool_use" : "end"
3267
- };
3151
+ return parseOpenAICompatibleOneShot(data);
3268
3152
  }
3269
3153
  };
3270
3154
 
3271
- // src/services/providers/zai/ZAIChatServiceProvider.ts
3272
- var ZAIChatServiceProvider = class {
3155
+ // src/services/providers/openrouter/OpenRouterChatServiceProvider.ts
3156
+ var OpenRouterChatServiceProvider = class {
3273
3157
  /**
3274
3158
  * Create a chat service instance
3159
+ * @param options Service options
3160
+ * @returns OpenRouterChatService instance
3275
3161
  */
3276
3162
  createChatService(options) {
3277
- const model = options.model || this.getDefaultModel();
3278
- const visionModel = options.visionModel || (this.supportsVisionForModel(model) ? model : this.getDefaultVisionModel());
3279
- if (options.visionModel && !this.supportsVisionForModel(options.visionModel)) {
3280
- throw new Error(
3281
- `Model ${options.visionModel} does not support vision capabilities.`
3282
- );
3283
- }
3163
+ const visionModel = resolveVisionModel({
3164
+ model: options.model,
3165
+ visionModel: options.visionModel,
3166
+ defaultModel: this.getDefaultModel(),
3167
+ defaultVisionModel: options.model || this.getDefaultModel(),
3168
+ supportsVisionForModel: (visionModel2) => this.supportsVisionForModel(visionModel2),
3169
+ validate: "explicit"
3170
+ });
3284
3171
  const tools = options.tools;
3285
- const thinking = options.thinking ?? { type: "disabled" };
3286
- return new ZAIChatService(
3172
+ const appName = options.appName;
3173
+ const appUrl = options.appUrl;
3174
+ return new OpenRouterChatService(
3287
3175
  options.apiKey,
3288
- model,
3176
+ options.model || this.getDefaultModel(),
3289
3177
  visionModel,
3290
3178
  tools,
3291
- options.endpoint || ENDPOINT_ZAI_CHAT_COMPLETIONS_API,
3179
+ options.endpoint,
3292
3180
  options.responseLength,
3293
- options.responseFormat,
3294
- thinking
3181
+ appName,
3182
+ appUrl,
3183
+ options.reasoning_effort,
3184
+ options.includeReasoning,
3185
+ options.reasoningMaxTokens
3295
3186
  );
3296
3187
  }
3297
3188
  /**
3298
3189
  * Get the provider name
3190
+ * @returns Provider name ('openrouter')
3299
3191
  */
3300
3192
  getProviderName() {
3301
- return "zai";
3193
+ return "openrouter";
3302
3194
  }
3303
3195
  /**
3304
3196
  * Get the list of supported models
3197
+ * Supports a curated list of OpenRouter models
3198
+ * @returns Array of supported model names
3305
3199
  */
3306
3200
  getSupportedModels() {
3307
3201
  return [
3308
- MODEL_GLM_4_7,
3309
- MODEL_GLM_4_7_FLASHX,
3310
- MODEL_GLM_4_7_FLASH,
3311
- MODEL_GLM_4_6,
3312
- MODEL_GLM_4_6V,
3313
- MODEL_GLM_4_6V_FLASHX,
3314
- MODEL_GLM_4_6V_FLASH
3202
+ // Free models
3203
+ MODEL_GPT_OSS_20B_FREE,
3204
+ MODEL_ZAI_GLM_4_5_AIR_FREE,
3205
+ // OpenAI models
3206
+ MODEL_OPENAI_GPT_5_1_CHAT,
3207
+ MODEL_OPENAI_GPT_5_1_CODEX,
3208
+ MODEL_OPENAI_GPT_5_MINI,
3209
+ MODEL_OPENAI_GPT_5_NANO,
3210
+ MODEL_OPENAI_GPT_4O,
3211
+ MODEL_OPENAI_GPT_4_1_MINI,
3212
+ MODEL_OPENAI_GPT_4_1_NANO,
3213
+ // Anthropic models
3214
+ MODEL_ANTHROPIC_CLAUDE_OPUS_4,
3215
+ MODEL_ANTHROPIC_CLAUDE_SONNET_4,
3216
+ MODEL_ANTHROPIC_CLAUDE_3_7_SONNET,
3217
+ MODEL_ANTHROPIC_CLAUDE_3_5_SONNET,
3218
+ MODEL_ANTHROPIC_CLAUDE_4_5_HAIKU,
3219
+ // Gemini models
3220
+ MODEL_GOOGLE_GEMINI_2_5_PRO,
3221
+ MODEL_GOOGLE_GEMINI_2_5_FLASH,
3222
+ MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE_PREVIEW_09_2025,
3223
+ // Z.ai models
3224
+ MODEL_ZAI_GLM_4_7_FLASH,
3225
+ MODEL_ZAI_GLM_4_5_AIR,
3226
+ // Other models
3227
+ MODEL_MOONSHOTAI_KIMI_K2_5
3315
3228
  ];
3316
3229
  }
3317
3230
  /**
3318
3231
  * Get the default model
3232
+ * @returns Default model name (gpt-oss-20b:free)
3319
3233
  */
3320
3234
  getDefaultModel() {
3321
- return MODEL_GLM_4_7;
3235
+ return MODEL_GPT_OSS_20B_FREE;
3322
3236
  }
3323
3237
  /**
3324
- * Get the default vision model
3238
+ * Check if this provider supports vision (image processing)
3239
+ * @returns Vision support status (false - gpt-oss-20b does not support vision)
3325
3240
  */
3326
- getDefaultVisionModel() {
3327
- return MODEL_GLM_4_6V_FLASH;
3241
+ supportsVision() {
3242
+ return this.getSupportedModels().some(
3243
+ (model) => this.supportsVisionForModel(model)
3244
+ );
3328
3245
  }
3329
3246
  /**
3330
- * Check if this provider supports vision
3247
+ * Check if a specific model supports vision capabilities
3248
+ * @param model The model name to check
3249
+ * @returns True if the model supports vision, false otherwise
3331
3250
  */
3332
- supportsVision() {
3333
- return true;
3251
+ supportsVisionForModel(model) {
3252
+ return isOpenRouterVisionModel(model);
3334
3253
  }
3335
3254
  /**
3336
- * Check if a specific model supports vision capabilities
3255
+ * Get list of free tier models
3256
+ * @returns Array of free model names
3257
+ */
3258
+ getFreeModels() {
3259
+ return OPENROUTER_FREE_MODELS;
3260
+ }
3261
+ /**
3262
+ * Check if a model is free tier
3263
+ * @param model Model name to check
3264
+ * @returns True if the model is free
3337
3265
  */
3338
- supportsVisionForModel(model) {
3339
- return isZaiVisionModel(model);
3266
+ isModelFree(model) {
3267
+ return OPENROUTER_FREE_MODELS.includes(model) || model.endsWith(":free");
3340
3268
  }
3341
3269
  };
3342
3270
 
3343
- // src/services/providers/kimi/KimiChatService.ts
3344
- var KimiChatService = class {
3271
+ // src/services/providers/zai/ZAIChatService.ts
3272
+ var ZAIChatService = class {
3345
3273
  /**
3346
3274
  * Constructor
3347
- * @param apiKey Kimi API key
3275
+ * @param apiKey Z.ai API key
3348
3276
  * @param model Name of the model to use
3349
3277
  * @param visionModel Name of the vision model
3350
3278
  */
3351
- constructor(apiKey, model = MODEL_KIMI_K2_5, visionModel = MODEL_KIMI_K2_5, tools, endpoint = ENDPOINT_KIMI_CHAT_COMPLETIONS_API, responseLength, responseFormat, thinking) {
3279
+ constructor(apiKey, model = MODEL_GLM_4_7, visionModel = MODEL_GLM_4_6V_FLASH, tools, endpoint = ENDPOINT_ZAI_CHAT_COMPLETIONS_API, responseLength, responseFormat, thinking) {
3352
3280
  /** Provider name */
3353
- this.provider = "kimi";
3281
+ this.provider = "zai";
3354
3282
  this.apiKey = apiKey;
3355
3283
  this.model = model;
3356
3284
  this.tools = tools || [];
3357
3285
  this.endpoint = endpoint;
3358
3286
  this.responseLength = responseLength;
3359
3287
  this.responseFormat = responseFormat;
3360
- this.thinking = thinking ?? { type: "enabled" };
3288
+ this.thinking = thinking ?? { type: "disabled" };
3361
3289
  this.visionModel = visionModel;
3362
3290
  }
3363
3291
  /**
@@ -3376,57 +3304,43 @@ If it's in another language, summarize in that language.
3376
3304
  * Process chat messages
3377
3305
  */
3378
3306
  async processChat(messages, onPartialResponse, onCompleteResponse) {
3379
- if (this.tools.length === 0) {
3380
- const res = await this.callKimi(messages, this.model, true);
3381
- const full = await this.handleStream(res, onPartialResponse);
3382
- await onCompleteResponse(full);
3383
- return;
3384
- }
3385
- const { blocks, stop_reason } = await this.chatOnce(messages);
3386
- if (stop_reason === "end") {
3387
- const full = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
3388
- await onCompleteResponse(full);
3389
- return;
3390
- }
3391
- throw new Error(
3392
- "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
3393
- );
3307
+ await processChatWithOptionalTools({
3308
+ hasTools: this.tools.length > 0,
3309
+ runWithoutTools: async () => {
3310
+ const res = await this.callZAI(messages, this.model, true);
3311
+ return this.handleStream(res, onPartialResponse);
3312
+ },
3313
+ runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
3314
+ onCompleteResponse,
3315
+ toolErrorMessage: "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
3316
+ });
3394
3317
  }
3395
3318
  /**
3396
3319
  * Process chat messages with images
3397
3320
  */
3398
3321
  async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
3399
- if (!isKimiVisionModel(this.visionModel)) {
3322
+ if (!isZaiVisionModel(this.visionModel)) {
3400
3323
  throw new Error(
3401
3324
  `Model ${this.visionModel} does not support vision capabilities.`
3402
3325
  );
3403
3326
  }
3404
- if (this.tools.length === 0) {
3405
- const res = await this.callKimi(messages, this.visionModel, true);
3406
- const full = await this.handleStream(res, onPartialResponse);
3407
- await onCompleteResponse(full);
3408
- return;
3409
- }
3410
- const { blocks, stop_reason } = await this.visionChatOnce(
3411
- messages,
3412
- true,
3413
- onPartialResponse
3414
- );
3415
- if (stop_reason === "end") {
3416
- const full = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
3417
- await onCompleteResponse(full);
3418
- return;
3419
- }
3420
- throw new Error(
3421
- "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
3422
- );
3327
+ await processChatWithOptionalTools({
3328
+ hasTools: this.tools.length > 0,
3329
+ runWithoutTools: async () => {
3330
+ const res = await this.callZAI(messages, this.visionModel, true);
3331
+ return this.handleStream(res, onPartialResponse);
3332
+ },
3333
+ runWithTools: () => this.visionChatOnce(messages, true, onPartialResponse),
3334
+ onCompleteResponse,
3335
+ toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
3336
+ });
3423
3337
  }
3424
3338
  /**
3425
3339
  * Process chat messages with tools (text only)
3426
3340
  */
3427
3341
  async chatOnce(messages, stream = true, onPartialResponse = () => {
3428
3342
  }, maxTokens) {
3429
- const res = await this.callKimi(messages, this.model, stream, maxTokens);
3343
+ const res = await this.callZAI(messages, this.model, stream, maxTokens);
3430
3344
  return this.parseResponse(res, stream, onPartialResponse);
3431
3345
  }
3432
3346
  /**
@@ -3434,12 +3348,12 @@ If it's in another language, summarize in that language.
3434
3348
  */
3435
3349
  async visionChatOnce(messages, stream = false, onPartialResponse = () => {
3436
3350
  }, maxTokens) {
3437
- if (!isKimiVisionModel(this.visionModel)) {
3351
+ if (!isZaiVisionModel(this.visionModel)) {
3438
3352
  throw new Error(
3439
3353
  `Model ${this.visionModel} does not support vision capabilities.`
3440
3354
  );
3441
3355
  }
3442
- const res = await this.callKimi(
3356
+ const res = await this.callZAI(
3443
3357
  messages,
3444
3358
  this.visionModel,
3445
3359
  stream,
@@ -3450,7 +3364,7 @@ If it's in another language, summarize in that language.
3450
3364
  async parseResponse(res, stream, onPartialResponse) {
3451
3365
  return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
3452
3366
  }
3453
- async callKimi(messages, model, stream = false, maxTokens) {
3367
+ async callZAI(messages, model, stream = false, maxTokens) {
3454
3368
  const body = this.buildRequestBody(messages, model, stream, maxTokens);
3455
3369
  const res = await ChatServiceHttpClient.post(this.endpoint, body, {
3456
3370
  Authorization: `Bearer ${this.apiKey}`
@@ -3473,179 +3387,66 @@ If it's in another language, summarize in that language.
3473
3387
  if (this.responseFormat) {
3474
3388
  body.response_format = this.responseFormat;
3475
3389
  }
3476
- const effectiveThinking = this.tools.length > 0 ? { type: "disabled" } : this.thinking;
3477
- if (effectiveThinking) {
3478
- if (this.isSelfHostedEndpoint()) {
3479
- if (effectiveThinking.type === "disabled") {
3480
- body.chat_template_kwargs = { thinking: false };
3481
- }
3482
- } else {
3483
- body.thinking = effectiveThinking;
3484
- }
3390
+ if (this.thinking) {
3391
+ body.thinking = this.thinking;
3485
3392
  }
3486
3393
  const tools = this.buildToolsDefinition();
3487
3394
  if (tools.length > 0) {
3488
3395
  body.tools = tools;
3489
3396
  body.tool_choice = "auto";
3397
+ if (stream && isZaiToolStreamModel(model)) {
3398
+ body.tool_stream = true;
3399
+ }
3490
3400
  }
3491
3401
  return body;
3492
3402
  }
3493
- isSelfHostedEndpoint() {
3494
- return this.normalizeEndpoint(this.endpoint) !== this.normalizeEndpoint(ENDPOINT_KIMI_CHAT_COMPLETIONS_API);
3495
- }
3496
- normalizeEndpoint(value) {
3497
- return value.replace(/\/+$/, "");
3498
- }
3499
3403
  buildToolsDefinition() {
3500
- if (this.tools.length === 0) return [];
3501
- return this.tools.map((t) => ({
3502
- type: "function",
3503
- function: {
3504
- name: t.name,
3505
- description: t.description,
3506
- parameters: t.parameters
3507
- }
3508
- }));
3404
+ return buildOpenAICompatibleTools(this.tools, "chat-completions");
3509
3405
  }
3510
3406
  async handleStream(res, onPartial) {
3511
- const reader = res.body.getReader();
3512
- const dec = new TextDecoder();
3513
- let full = "";
3514
- let buf = "";
3515
- while (true) {
3516
- const { done, value } = await reader.read();
3517
- if (done) break;
3518
- buf += dec.decode(value, { stream: true });
3519
- const lines = buf.split("\n");
3520
- buf = lines.pop() || "";
3521
- for (const line of lines) {
3522
- const trimmedLine = line.trim();
3523
- if (!trimmedLine || trimmedLine.startsWith(":")) continue;
3524
- if (!trimmedLine.startsWith("data:")) continue;
3525
- const payload = trimmedLine.slice(5).trim();
3526
- if (payload === "[DONE]") {
3527
- break;
3528
- }
3529
- try {
3530
- const json = JSON.parse(payload);
3531
- const content = json.choices?.[0]?.delta?.content || "";
3532
- if (content) {
3533
- onPartial(content);
3534
- full += content;
3535
- }
3536
- } catch (e) {
3537
- console.debug("Failed to parse SSE data:", payload);
3538
- }
3539
- }
3540
- }
3541
- return full;
3407
+ return parseOpenAICompatibleTextStream(res, onPartial, {
3408
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3409
+ });
3542
3410
  }
3543
3411
  /**
3544
3412
  * Parse streaming response with tool support
3545
3413
  */
3546
3414
  async parseStream(res, onPartial) {
3547
- const reader = res.body.getReader();
3548
- const dec = new TextDecoder();
3549
- const textBlocks = [];
3550
- const toolCallsMap = /* @__PURE__ */ new Map();
3551
- let buf = "";
3552
- while (true) {
3553
- const { done, value } = await reader.read();
3554
- if (done) break;
3555
- buf += dec.decode(value, { stream: true });
3556
- const lines = buf.split("\n");
3557
- buf = lines.pop() || "";
3558
- for (const line of lines) {
3559
- const trimmedLine = line.trim();
3560
- if (!trimmedLine || trimmedLine.startsWith(":")) continue;
3561
- if (!trimmedLine.startsWith("data:")) continue;
3562
- const payload = trimmedLine.slice(5).trim();
3563
- if (payload === "[DONE]") {
3564
- break;
3565
- }
3566
- try {
3567
- const json = JSON.parse(payload);
3568
- const delta = json.choices?.[0]?.delta;
3569
- if (delta?.content) {
3570
- onPartial(delta.content);
3571
- StreamTextAccumulator.append(textBlocks, delta.content);
3572
- }
3573
- if (delta?.tool_calls) {
3574
- delta.tool_calls.forEach((c) => {
3575
- const entry = toolCallsMap.get(c.index) ?? {
3576
- id: c.id,
3577
- name: c.function?.name,
3578
- args: ""
3579
- };
3580
- entry.args += c.function?.arguments || "";
3581
- toolCallsMap.set(c.index, entry);
3582
- });
3583
- }
3584
- } catch (e) {
3585
- console.debug("Failed to parse SSE data:", payload);
3586
- }
3587
- }
3588
- }
3589
- const toolBlocks = Array.from(toolCallsMap.entries()).sort((a, b) => a[0] - b[0]).map(([_, e]) => ({
3590
- type: "tool_use",
3591
- id: e.id,
3592
- name: e.name,
3593
- input: JSON.parse(e.args || "{}")
3594
- }));
3595
- const blocks = [...textBlocks, ...toolBlocks];
3596
- return {
3597
- blocks,
3598
- stop_reason: toolBlocks.length ? "tool_use" : "end"
3599
- };
3415
+ return parseOpenAICompatibleToolStream(res, onPartial, {
3416
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3417
+ });
3600
3418
  }
3601
3419
  /**
3602
3420
  * Parse non-streaming response
3603
3421
  */
3604
3422
  parseOneShot(data) {
3605
- const choice = data.choices?.[0];
3606
- const blocks = [];
3607
- if (choice?.message?.tool_calls?.length) {
3608
- choice.message.tool_calls.forEach(
3609
- (c) => blocks.push({
3610
- type: "tool_use",
3611
- id: c.id,
3612
- name: c.function?.name,
3613
- input: JSON.parse(c.function?.arguments || "{}")
3614
- })
3615
- );
3616
- } else if (choice?.message?.content) {
3617
- blocks.push({ type: "text", text: choice.message.content });
3618
- }
3619
- return {
3620
- blocks,
3621
- stop_reason: blocks.some((b) => b.type === "tool_use") ? "tool_use" : "end"
3622
- };
3423
+ return parseOpenAICompatibleOneShot(data);
3623
3424
  }
3624
3425
  };
3625
3426
 
3626
- // src/services/providers/kimi/KimiChatServiceProvider.ts
3627
- var KimiChatServiceProvider = class {
3427
+ // src/services/providers/zai/ZAIChatServiceProvider.ts
3428
+ var ZAIChatServiceProvider = class {
3628
3429
  /**
3629
3430
  * Create a chat service instance
3630
3431
  */
3631
3432
  createChatService(options) {
3632
- const endpoint = this.resolveEndpoint(options);
3633
3433
  const model = options.model || this.getDefaultModel();
3634
- const visionModel = options.visionModel || (this.supportsVisionForModel(model) ? model : this.getDefaultVisionModel());
3635
- if (options.visionModel && !this.supportsVisionForModel(options.visionModel)) {
3636
- throw new Error(
3637
- `Model ${options.visionModel} does not support vision capabilities.`
3638
- );
3639
- }
3434
+ const visionModel = resolveVisionModel({
3435
+ model,
3436
+ visionModel: options.visionModel,
3437
+ defaultModel: this.getDefaultModel(),
3438
+ defaultVisionModel: this.getDefaultVisionModel(),
3439
+ supportsVisionForModel: (visionModel2) => this.supportsVisionForModel(visionModel2),
3440
+ validate: "explicit"
3441
+ });
3640
3442
  const tools = options.tools;
3641
- const defaultThinking = options.thinking ?? { type: "enabled" };
3642
- const thinking = tools && tools.length > 0 ? { type: "disabled" } : defaultThinking;
3643
- return new KimiChatService(
3443
+ const thinking = options.thinking ?? { type: "disabled" };
3444
+ return new ZAIChatService(
3644
3445
  options.apiKey,
3645
3446
  model,
3646
3447
  visionModel,
3647
3448
  tools,
3648
- endpoint,
3449
+ options.endpoint || ENDPOINT_ZAI_CHAT_COMPLETIONS_API,
3649
3450
  options.responseLength,
3650
3451
  options.responseFormat,
3651
3452
  thinking
@@ -3655,25 +3456,33 @@ If it's in another language, summarize in that language.
3655
3456
  * Get the provider name
3656
3457
  */
3657
3458
  getProviderName() {
3658
- return "kimi";
3459
+ return "zai";
3659
3460
  }
3660
3461
  /**
3661
3462
  * Get the list of supported models
3662
3463
  */
3663
3464
  getSupportedModels() {
3664
- return [MODEL_KIMI_K2_5];
3465
+ return [
3466
+ MODEL_GLM_4_7,
3467
+ MODEL_GLM_4_7_FLASHX,
3468
+ MODEL_GLM_4_7_FLASH,
3469
+ MODEL_GLM_4_6,
3470
+ MODEL_GLM_4_6V,
3471
+ MODEL_GLM_4_6V_FLASHX,
3472
+ MODEL_GLM_4_6V_FLASH
3473
+ ];
3665
3474
  }
3666
3475
  /**
3667
3476
  * Get the default model
3668
3477
  */
3669
3478
  getDefaultModel() {
3670
- return MODEL_KIMI_K2_5;
3479
+ return MODEL_GLM_4_7;
3671
3480
  }
3672
3481
  /**
3673
3482
  * Get the default vision model
3674
3483
  */
3675
3484
  getDefaultVisionModel() {
3676
- return MODEL_KIMI_K2_5;
3485
+ return MODEL_GLM_4_6V_FLASH;
3677
3486
  }
3678
3487
  /**
3679
3488
  * Check if this provider supports vision
@@ -3685,26 +3494,20 @@ If it's in another language, summarize in that language.
3685
3494
  * Check if a specific model supports vision capabilities
3686
3495
  */
3687
3496
  supportsVisionForModel(model) {
3688
- return isKimiVisionModel(model);
3689
- }
3690
- resolveEndpoint(options) {
3691
- if (options.endpoint) {
3692
- return this.normalizeEndpoint(options.endpoint);
3693
- }
3694
- if (options.baseUrl) {
3695
- const baseUrl = this.normalizeEndpoint(options.baseUrl);
3696
- if (baseUrl.endsWith("/chat/completions")) {
3697
- return baseUrl;
3698
- }
3699
- return `${baseUrl}/chat/completions`;
3700
- }
3701
- return ENDPOINT_KIMI_CHAT_COMPLETIONS_API;
3702
- }
3703
- normalizeEndpoint(value) {
3704
- return value.replace(/\/+$/, "");
3497
+ return isZaiVisionModel(model);
3705
3498
  }
3706
3499
  };
3707
3500
 
3501
+ // src/services/providers/index.ts
3502
+ var DEFAULT_CHAT_SERVICE_PROVIDERS = [
3503
+ new OpenAIChatServiceProvider(),
3504
+ new GeminiChatServiceProvider(),
3505
+ new ClaudeChatServiceProvider(),
3506
+ new OpenRouterChatServiceProvider(),
3507
+ new ZAIChatServiceProvider(),
3508
+ new KimiChatServiceProvider()
3509
+ ];
3510
+
3708
3511
  // src/services/ChatServiceFactory.ts
3709
3512
  var ChatServiceFactory = class {
3710
3513
  /**
@@ -3714,12 +3517,6 @@ If it's in another language, summarize in that language.
3714
3517
  static registerProvider(provider) {
3715
3518
  this.providers.set(provider.getProviderName(), provider);
3716
3519
  }
3717
- /**
3718
- * Create a chat service with the specified provider name and options
3719
- * @param providerName Provider name
3720
- * @param options Service options
3721
- * @returns Created ChatService instance
3722
- */
3723
3520
  static createChatService(providerName, options) {
3724
3521
  const provider = this.providers.get(providerName);
3725
3522
  if (!provider) {
@@ -3753,89 +3550,9 @@ If it's in another language, summarize in that language.
3753
3550
  };
3754
3551
  /** Map of registered providers */
3755
3552
  ChatServiceFactory.providers = /* @__PURE__ */ new Map();
3756
- ChatServiceFactory.registerProvider(new OpenAIChatServiceProvider());
3757
- ChatServiceFactory.registerProvider(new GeminiChatServiceProvider());
3758
- ChatServiceFactory.registerProvider(new ClaudeChatServiceProvider());
3759
- ChatServiceFactory.registerProvider(new OpenRouterChatServiceProvider());
3760
- ChatServiceFactory.registerProvider(new ZAIChatServiceProvider());
3761
- ChatServiceFactory.registerProvider(new KimiChatServiceProvider());
3762
-
3763
- // src/utils/emotionParser.ts
3764
- var emotions = ["happy", "sad", "angry", "surprised", "neutral"];
3765
- var EMOTION_TAG_REGEX = /\[([a-z]+)\]/i;
3766
- var EMOTION_TAG_CLEANUP_REGEX = /\[[a-z]+\]\s*/gi;
3767
- var EmotionParser = class {
3768
- /**
3769
- * Extract emotion from text and return clean text
3770
- * @param text Text that may contain emotion tags like [happy]
3771
- * @returns Object containing extracted emotion and clean text
3772
- */
3773
- static extractEmotion(text) {
3774
- const match = text.match(EMOTION_TAG_REGEX);
3775
- if (match) {
3776
- const emotion = match[1].toLowerCase();
3777
- const cleanText = text.replace(EMOTION_TAG_CLEANUP_REGEX, "").trim();
3778
- return {
3779
- emotion,
3780
- cleanText
3781
- };
3782
- }
3783
- return { cleanText: text };
3784
- }
3785
- /**
3786
- * Check if an emotion is valid
3787
- * @param emotion Emotion string to validate
3788
- * @returns True if the emotion is valid
3789
- */
3790
- static isValidEmotion(emotion) {
3791
- return emotions.includes(emotion);
3792
- }
3793
- /**
3794
- * Remove all emotion tags from text
3795
- * @param text Text containing emotion tags
3796
- * @returns Clean text without emotion tags
3797
- */
3798
- static cleanEmotionTags(text) {
3799
- return text.replace(EMOTION_TAG_CLEANUP_REGEX, "").trim();
3800
- }
3801
- /**
3802
- * Add emotion tag to text
3803
- * @param emotion Emotion to add
3804
- * @param text Text content
3805
- * @returns Text with emotion tag prepended
3806
- */
3807
- static addEmotionTag(emotion, text) {
3808
- return `[${emotion}] ${text}`;
3809
- }
3810
- };
3811
-
3812
- // src/utils/screenplay.ts
3813
- function textToScreenplay(text) {
3814
- const { emotion, cleanText } = EmotionParser.extractEmotion(text);
3815
- if (emotion) {
3816
- return {
3817
- emotion,
3818
- text: cleanText
3819
- };
3820
- }
3821
- return { text: cleanText };
3822
- }
3823
- function textsToScreenplay(texts) {
3824
- return texts.map((text) => textToScreenplay(text));
3825
- }
3826
- function screenplayToText(screenplay) {
3827
- if (screenplay.emotion) {
3828
- return EmotionParser.addEmotionTag(screenplay.emotion, screenplay.text);
3829
- }
3830
- return screenplay.text;
3831
- }
3832
-
3833
- // src/utils/runOnce.ts
3834
- async function runOnceText(chat, messages) {
3835
- const { blocks } = await chat.chatOnce(messages, false, () => {
3836
- });
3837
- return StreamTextAccumulator.getFullText(blocks);
3838
- }
3553
+ DEFAULT_CHAT_SERVICE_PROVIDERS.forEach(
3554
+ (provider) => ChatServiceFactory.registerProvider(provider)
3555
+ );
3839
3556
 
3840
3557
  // src/adapters/gasFetch.ts
3841
3558
  function installGASFetch() {