@aituber-onair/chat 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) 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 +47 -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/utils/index.d.ts +4 -0
  63. package/dist/cjs/utils/index.d.ts.map +1 -1
  64. package/dist/cjs/utils/index.js +4 -0
  65. package/dist/cjs/utils/index.js.map +1 -1
  66. package/dist/cjs/utils/openaiCompatibleSse.d.ts +10 -0
  67. package/dist/cjs/utils/openaiCompatibleSse.d.ts.map +1 -0
  68. package/dist/cjs/utils/openaiCompatibleSse.js +124 -0
  69. package/dist/cjs/utils/openaiCompatibleSse.js.map +1 -0
  70. package/dist/cjs/utils/openaiCompatibleTools.d.ts +4 -0
  71. package/dist/cjs/utils/openaiCompatibleTools.d.ts.map +1 -0
  72. package/dist/cjs/utils/openaiCompatibleTools.js +25 -0
  73. package/dist/cjs/utils/openaiCompatibleTools.js.map +1 -0
  74. package/dist/cjs/utils/processChatFlow.d.ts +12 -0
  75. package/dist/cjs/utils/processChatFlow.d.ts.map +1 -0
  76. package/dist/cjs/utils/processChatFlow.js +22 -0
  77. package/dist/cjs/utils/processChatFlow.js.map +1 -0
  78. package/dist/cjs/utils/visionModelResolver.d.ts +12 -0
  79. package/dist/cjs/utils/visionModelResolver.d.ts.map +1 -0
  80. package/dist/cjs/utils/visionModelResolver.js +22 -0
  81. package/dist/cjs/utils/visionModelResolver.js.map +1 -0
  82. package/dist/esm/constants/claude.d.ts +1 -0
  83. package/dist/esm/constants/claude.d.ts.map +1 -1
  84. package/dist/esm/constants/claude.js +2 -0
  85. package/dist/esm/constants/claude.js.map +1 -1
  86. package/dist/esm/index.d.ts +1 -1
  87. package/dist/esm/index.d.ts.map +1 -1
  88. package/dist/esm/index.js.map +1 -1
  89. package/dist/esm/services/ChatServiceFactory.d.ts +2 -2
  90. package/dist/esm/services/ChatServiceFactory.d.ts.map +1 -1
  91. package/dist/esm/services/ChatServiceFactory.js +2 -24
  92. package/dist/esm/services/ChatServiceFactory.js.map +1 -1
  93. package/dist/esm/services/providers/ChatServiceProvider.d.ts +35 -5
  94. package/dist/esm/services/providers/ChatServiceProvider.d.ts.map +1 -1
  95. package/dist/esm/services/providers/claude/ClaudeChatService.d.ts.map +1 -1
  96. package/dist/esm/services/providers/claude/ClaudeChatService.js +21 -36
  97. package/dist/esm/services/providers/claude/ClaudeChatService.js.map +1 -1
  98. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.d.ts +3 -3
  99. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.d.ts.map +1 -1
  100. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.js +11 -5
  101. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.js.map +1 -1
  102. package/dist/esm/services/providers/gemini/GeminiChatService.d.ts.map +1 -1
  103. package/dist/esm/services/providers/gemini/GeminiChatService.js +28 -45
  104. package/dist/esm/services/providers/gemini/GeminiChatService.js.map +1 -1
  105. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.d.ts +3 -3
  106. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.d.ts.map +1 -1
  107. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.js +9 -4
  108. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.js.map +1 -1
  109. package/dist/esm/services/providers/index.d.ts +8 -0
  110. package/dist/esm/services/providers/index.d.ts.map +1 -0
  111. package/dist/esm/services/providers/index.js +15 -0
  112. package/dist/esm/services/providers/index.js.map +1 -0
  113. package/dist/esm/services/providers/kimi/KimiChatService.d.ts.map +1 -1
  114. package/dist/esm/services/providers/kimi/KimiChatService.js +31 -162
  115. package/dist/esm/services/providers/kimi/KimiChatService.js.map +1 -1
  116. package/dist/esm/services/providers/kimi/KimiChatServiceProvider.d.ts +3 -3
  117. package/dist/esm/services/providers/kimi/KimiChatServiceProvider.d.ts.map +1 -1
  118. package/dist/esm/services/providers/kimi/KimiChatServiceProvider.js +9 -8
  119. package/dist/esm/services/providers/kimi/KimiChatServiceProvider.js.map +1 -1
  120. package/dist/esm/services/providers/openai/OpenAIChatService.d.ts.map +1 -1
  121. package/dist/esm/services/providers/openai/OpenAIChatService.js +47 -199
  122. package/dist/esm/services/providers/openai/OpenAIChatService.js.map +1 -1
  123. package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.d.ts +3 -3
  124. package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.d.ts.map +1 -1
  125. package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.js +9 -4
  126. package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.js.map +1 -1
  127. package/dist/esm/services/providers/openrouter/OpenRouterChatService.d.ts.map +1 -1
  128. package/dist/esm/services/providers/openrouter/OpenRouterChatService.js +31 -165
  129. package/dist/esm/services/providers/openrouter/OpenRouterChatService.js.map +1 -1
  130. package/dist/esm/services/providers/openrouter/OpenRouterChatServiceProvider.d.ts +3 -3
  131. package/dist/esm/services/providers/openrouter/OpenRouterChatServiceProvider.d.ts.map +1 -1
  132. package/dist/esm/services/providers/openrouter/OpenRouterChatServiceProvider.js +9 -6
  133. package/dist/esm/services/providers/openrouter/OpenRouterChatServiceProvider.js.map +1 -1
  134. package/dist/esm/services/providers/zai/ZAIChatService.d.ts.map +1 -1
  135. package/dist/esm/services/providers/zai/ZAIChatService.js +31 -162
  136. package/dist/esm/services/providers/zai/ZAIChatService.js.map +1 -1
  137. package/dist/esm/services/providers/zai/ZAIChatServiceProvider.d.ts +3 -3
  138. package/dist/esm/services/providers/zai/ZAIChatServiceProvider.d.ts.map +1 -1
  139. package/dist/esm/services/providers/zai/ZAIChatServiceProvider.js +9 -8
  140. package/dist/esm/services/providers/zai/ZAIChatServiceProvider.js.map +1 -1
  141. package/dist/esm/utils/index.d.ts +4 -0
  142. package/dist/esm/utils/index.d.ts.map +1 -1
  143. package/dist/esm/utils/index.js +4 -0
  144. package/dist/esm/utils/index.js.map +1 -1
  145. package/dist/esm/utils/openaiCompatibleSse.d.ts +10 -0
  146. package/dist/esm/utils/openaiCompatibleSse.d.ts.map +1 -0
  147. package/dist/esm/utils/openaiCompatibleSse.js +119 -0
  148. package/dist/esm/utils/openaiCompatibleSse.js.map +1 -0
  149. package/dist/esm/utils/openaiCompatibleTools.d.ts +4 -0
  150. package/dist/esm/utils/openaiCompatibleTools.d.ts.map +1 -0
  151. package/dist/esm/utils/openaiCompatibleTools.js +21 -0
  152. package/dist/esm/utils/openaiCompatibleTools.js.map +1 -0
  153. package/dist/esm/utils/processChatFlow.d.ts +12 -0
  154. package/dist/esm/utils/processChatFlow.d.ts.map +1 -0
  155. package/dist/esm/utils/processChatFlow.js +19 -0
  156. package/dist/esm/utils/processChatFlow.js.map +1 -0
  157. package/dist/esm/utils/visionModelResolver.d.ts +12 -0
  158. package/dist/esm/utils/visionModelResolver.d.ts.map +1 -0
  159. package/dist/esm/utils/visionModelResolver.js +18 -0
  160. package/dist/esm/utils/visionModelResolver.js.map +1 -0
  161. package/dist/umd/aituber-onair-chat.js +1586 -1872
  162. package/dist/umd/aituber-onair-chat.min.js +6 -15
  163. 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
- };
2185
+ const tools = this.buildToolsDefinition();
2186
+ if (tools.length > 0) {
2187
+ body.tools = tools;
2188
+ body.tool_choice = "auto";
2189
+ }
2190
+ return body;
2363
2191
  }
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("");
2192
+ isSelfHostedEndpoint() {
2193
+ return this.normalizeEndpoint(this.endpoint) !== this.normalizeEndpoint(ENDPOINT_KIMI_CHAT_COMPLETIONS_API);
2367
2194
  }
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
- };
2195
+ normalizeEndpoint(value) {
2196
+ return value.replace(/\/+$/, "");
2197
+ }
2198
+ buildToolsDefinition() {
2199
+ return buildOpenAICompatibleTools(this.tools, "chat-completions");
2409
2200
  }
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);
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,189 @@ 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.tool_configuration?.allowed_tools) {
2599
+ mcpDef.allowed_tools = server.tool_configuration.allowed_tools;
2600
+ }
2601
+ if (server.authorization_token) {
2602
+ mcpDef.headers = {
2603
+ Authorization: `Bearer ${server.authorization_token}`
2604
+ };
2605
+ }
2606
+ return mcpDef;
2607
+ });
2608
+ }
2609
+ async handleStream(res, onPartial) {
2610
+ return parseOpenAICompatibleTextStream(res, onPartial);
2611
+ }
2804
2612
  async parseStream(res, onPartial) {
2613
+ return parseOpenAICompatibleToolStream(res, onPartial, {
2614
+ appendTextBlock: StreamTextAccumulator.addTextBlock
2615
+ });
2616
+ }
2617
+ parseOneShot(data) {
2618
+ return parseOpenAICompatibleOneShot(data);
2619
+ }
2620
+ /**
2621
+ * Parse streaming response from Responses API (SSE format)
2622
+ */
2623
+ async parseResponsesStream(res, onPartial) {
2805
2624
  const reader = res.body.getReader();
2806
2625
  const dec = new TextDecoder();
2807
2626
  const textBlocks = [];
@@ -2811,164 +2630,221 @@ If it's in another language, summarize in that language.
2811
2630
  const { done, value } = await reader.read();
2812
2631
  if (done) break;
2813
2632
  buf += dec.decode(value, { stream: true });
2633
+ let eventType = "";
2634
+ let eventData = "";
2814
2635
  const lines = buf.split("\n");
2815
2636
  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
- });
2637
+ for (let i = 0; i < lines.length; i++) {
2638
+ const line = lines[i].trim();
2639
+ if (line.startsWith("event:")) {
2640
+ eventType = line.slice(6).trim();
2641
+ } else if (line.startsWith("data:")) {
2642
+ eventData = line.slice(5).trim();
2643
+ } else if (line === "" && eventType && eventData) {
2644
+ try {
2645
+ const json = JSON.parse(eventData);
2646
+ const completionResult = this.handleResponsesSSEEvent(
2647
+ eventType,
2648
+ json,
2649
+ onPartial,
2650
+ textBlocks,
2651
+ toolCallsMap
2652
+ );
2653
+ if (completionResult === "completed") {
2654
+ }
2655
+ } catch (e) {
2656
+ console.warn("Failed to parse SSE data:", eventData);
2841
2657
  }
2842
- } catch (e) {
2843
- console.debug("Failed to parse SSE data:", payload);
2658
+ eventType = "";
2659
+ eventData = "";
2844
2660
  }
2845
2661
  }
2846
2662
  }
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
- };
2663
+ const toolBlocks = Array.from(toolCallsMap.values()).map(
2664
+ (tool) => ({
2665
+ type: "tool_use",
2666
+ id: tool.id,
2667
+ name: tool.name,
2668
+ input: tool.input || {}
2669
+ })
2670
+ );
2671
+ const blocks = [...textBlocks, ...toolBlocks];
2672
+ return {
2673
+ blocks,
2674
+ stop_reason: toolBlocks.length ? "tool_use" : "end"
2675
+ };
2676
+ }
2677
+ /**
2678
+ * Handle specific SSE events from Responses API
2679
+ * @returns 'completed' if the response is completed, undefined otherwise
2680
+ */
2681
+ handleResponsesSSEEvent(eventType, data, onPartial, textBlocks, toolCallsMap) {
2682
+ switch (eventType) {
2683
+ // Item addition events
2684
+ case "response.output_item.added":
2685
+ if (data.item?.type === "message" && Array.isArray(data.item.content)) {
2686
+ data.item.content.forEach((c) => {
2687
+ if (c.type === "output_text" && c.text) {
2688
+ onPartial(c.text);
2689
+ StreamTextAccumulator.append(textBlocks, c.text);
2690
+ }
2691
+ });
2692
+ } else if (data.item?.type === "function_call") {
2693
+ toolCallsMap.set(data.item.id, {
2694
+ id: data.item.id,
2695
+ name: data.item.name,
2696
+ input: data.item.arguments ? JSON.parse(data.item.arguments) : {}
2697
+ });
2698
+ }
2699
+ break;
2700
+ // Initial content part events
2701
+ case "response.content_part.added":
2702
+ if (data.part?.type === "output_text" && typeof data.part.text === "string") {
2703
+ onPartial(data.part.text);
2704
+ StreamTextAccumulator.append(textBlocks, data.part.text);
2705
+ }
2706
+ break;
2707
+ // Text delta events
2708
+ case "response.output_text.delta":
2709
+ case "response.content_part.delta":
2710
+ {
2711
+ const deltaText = typeof data.delta === "string" ? data.delta : data.delta?.text ?? "";
2712
+ if (deltaText) {
2713
+ onPartial(deltaText);
2714
+ StreamTextAccumulator.append(textBlocks, deltaText);
2715
+ }
2716
+ }
2717
+ break;
2718
+ // Text completion events - do not add text here as it's already accumulated via delta events
2719
+ case "response.output_text.done":
2720
+ case "response.content_part.done":
2721
+ break;
2722
+ // Response completion events
2723
+ case "response.completed":
2724
+ return "completed";
2725
+ // GPT-5 reasoning token events (not visible but counted for billing)
2726
+ case "response.reasoning.started":
2727
+ case "response.reasoning.delta":
2728
+ case "response.reasoning.done":
2729
+ break;
2730
+ default:
2731
+ break;
2732
+ }
2733
+ return void 0;
2858
2734
  }
2859
2735
  /**
2860
- * Parse non-streaming response
2736
+ * Parse non-streaming response from Responses API
2861
2737
  */
2862
- parseOneShot(data) {
2863
- const choice = data.choices?.[0];
2738
+ parseResponsesOneShot(data) {
2864
2739
  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 });
2740
+ if (data.output && Array.isArray(data.output)) {
2741
+ data.output.forEach((outputItem) => {
2742
+ if (outputItem.type === "message" && outputItem.content) {
2743
+ outputItem.content.forEach((content) => {
2744
+ if (content.type === "output_text" && content.text) {
2745
+ blocks.push({ type: "text", text: content.text });
2746
+ }
2747
+ });
2748
+ }
2749
+ if (outputItem.type === "function_call") {
2750
+ blocks.push({
2751
+ type: "tool_use",
2752
+ id: outputItem.id,
2753
+ name: outputItem.name,
2754
+ input: outputItem.arguments ? JSON.parse(outputItem.arguments) : {}
2755
+ });
2756
+ }
2757
+ });
2876
2758
  }
2877
2759
  return {
2878
2760
  blocks,
2879
- stop_reason: choice?.finish_reason === "tool_calls" ? "tool_use" : "end"
2761
+ stop_reason: blocks.some((b) => b.type === "tool_use") ? "tool_use" : "end"
2880
2762
  };
2881
2763
  }
2882
2764
  };
2883
2765
 
2884
- // src/services/providers/openrouter/OpenRouterChatServiceProvider.ts
2885
- var OpenRouterChatServiceProvider = class {
2766
+ // src/services/providers/openai/OpenAIChatServiceProvider.ts
2767
+ var OpenAIChatServiceProvider = class {
2886
2768
  /**
2887
2769
  * Create a chat service instance
2888
2770
  * @param options Service options
2889
- * @returns OpenRouterChatService instance
2771
+ * @returns OpenAIChatService instance
2890
2772
  */
2891
2773
  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
- );
2774
+ const optimizedOptions = this.optimizeGPT5Options(options);
2775
+ const visionModel = resolveVisionModel({
2776
+ model: optimizedOptions.model,
2777
+ visionModel: optimizedOptions.visionModel,
2778
+ defaultModel: this.getDefaultModel(),
2779
+ defaultVisionModel: this.getDefaultModel(),
2780
+ supportsVisionForModel: (model) => this.supportsVisionForModel(model),
2781
+ validate: "resolved"
2782
+ });
2783
+ const tools = optimizedOptions.tools;
2784
+ const mcpServers = optimizedOptions.mcpServers ?? [];
2785
+ const modelName = optimizedOptions.model || this.getDefaultModel();
2786
+ let shouldUseResponsesAPI = false;
2787
+ if (mcpServers.length > 0) {
2788
+ shouldUseResponsesAPI = true;
2789
+ } else if (isGPT5Model(modelName)) {
2790
+ const preference = optimizedOptions.gpt5EndpointPreference || "chat";
2791
+ shouldUseResponsesAPI = preference === "responses";
2897
2792
  }
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(),
2793
+ const endpoint = optimizedOptions.endpoint || (shouldUseResponsesAPI ? ENDPOINT_OPENAI_RESPONSES_API : ENDPOINT_OPENAI_CHAT_COMPLETIONS_API);
2794
+ return new OpenAIChatService(
2795
+ optimizedOptions.apiKey,
2796
+ modelName,
2904
2797
  visionModel,
2905
2798
  tools,
2906
- options.endpoint,
2907
- options.responseLength,
2908
- appName,
2909
- appUrl,
2910
- options.reasoning_effort,
2911
- options.includeReasoning,
2912
- options.reasoningMaxTokens
2799
+ endpoint,
2800
+ mcpServers,
2801
+ optimizedOptions.responseLength,
2802
+ optimizedOptions.verbosity,
2803
+ optimizedOptions.reasoning_effort,
2804
+ optimizedOptions.enableReasoningSummary
2913
2805
  );
2914
2806
  }
2915
2807
  /**
2916
2808
  * Get the provider name
2917
- * @returns Provider name ('openrouter')
2809
+ * @returns Provider name ('openai')
2918
2810
  */
2919
2811
  getProviderName() {
2920
- return "openrouter";
2812
+ return "openai";
2921
2813
  }
2922
2814
  /**
2923
2815
  * Get the list of supported models
2924
- * Supports a curated list of OpenRouter models
2925
2816
  * @returns Array of supported model names
2926
2817
  */
2927
2818
  getSupportedModels() {
2928
2819
  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
2820
+ MODEL_GPT_5_NANO,
2821
+ MODEL_GPT_5_MINI,
2822
+ MODEL_GPT_5,
2823
+ MODEL_GPT_5_1,
2824
+ MODEL_GPT_4_1,
2825
+ MODEL_GPT_4_1_MINI,
2826
+ MODEL_GPT_4_1_NANO,
2827
+ MODEL_GPT_4O_MINI,
2828
+ MODEL_GPT_4O,
2829
+ MODEL_O3_MINI,
2830
+ MODEL_O1_MINI,
2831
+ MODEL_O1,
2832
+ MODEL_GPT_4_5_PREVIEW
2955
2833
  ];
2956
2834
  }
2957
2835
  /**
2958
2836
  * Get the default model
2959
- * @returns Default model name (gpt-oss-20b:free)
2837
+ * @returns Default model name
2960
2838
  */
2961
2839
  getDefaultModel() {
2962
- return MODEL_GPT_OSS_20B_FREE;
2840
+ return MODEL_GPT_5_NANO;
2963
2841
  }
2964
2842
  /**
2965
2843
  * Check if this provider supports vision (image processing)
2966
- * @returns Vision support status (false - gpt-oss-20b does not support vision)
2844
+ * @returns Vision support status (true)
2967
2845
  */
2968
2846
  supportsVision() {
2969
- return this.getSupportedModels().some(
2970
- (model) => this.supportsVisionForModel(model)
2971
- );
2847
+ return true;
2972
2848
  }
2973
2849
  /**
2974
2850
  * Check if a specific model supports vision capabilities
@@ -2976,388 +2852,437 @@ If it's in another language, summarize in that language.
2976
2852
  * @returns True if the model supports vision, false otherwise
2977
2853
  */
2978
2854
  supportsVisionForModel(model) {
2979
- return isOpenRouterVisionModel(model);
2855
+ return VISION_SUPPORTED_MODELS.includes(model);
2980
2856
  }
2981
2857
  /**
2982
- * Get list of free tier models
2983
- * @returns Array of free model names
2858
+ * Apply GPT-5 specific optimizations to options
2859
+ * @param options Original chat service options
2860
+ * @returns Optimized options for GPT-5 usage
2984
2861
  */
2985
- getFreeModels() {
2986
- return OPENROUTER_FREE_MODELS;
2862
+ optimizeGPT5Options(options) {
2863
+ const modelName = options.model || this.getDefaultModel();
2864
+ if (!isGPT5Model(modelName)) {
2865
+ return options;
2866
+ }
2867
+ const optimized = { ...options };
2868
+ if (options.gpt5Preset) {
2869
+ const preset = GPT5_PRESETS[options.gpt5Preset];
2870
+ optimized.reasoning_effort = preset.reasoning_effort;
2871
+ optimized.verbosity = preset.verbosity;
2872
+ } else {
2873
+ if (!options.reasoning_effort) {
2874
+ optimized.reasoning_effort = this.getDefaultReasoningEffortForModel(modelName);
2875
+ }
2876
+ }
2877
+ optimized.reasoning_effort = this.normalizeReasoningEffort(
2878
+ modelName,
2879
+ optimized.reasoning_effort
2880
+ );
2881
+ return optimized;
2987
2882
  }
2988
2883
  /**
2989
- * Check if a model is free tier
2990
- * @param model Model name to check
2991
- * @returns True if the model is free
2884
+ * Determine the default reasoning effort for GPT-5 family models
2885
+ * GPT-5.1 defaults to 'none' (fastest), earlier GPT-5 defaults to 'medium'
2992
2886
  */
2993
- isModelFree(model) {
2994
- return OPENROUTER_FREE_MODELS.includes(model) || model.endsWith(":free");
2887
+ getDefaultReasoningEffortForModel(modelName) {
2888
+ if (modelName === MODEL_GPT_5_1) {
2889
+ return "none";
2890
+ }
2891
+ return "medium";
2892
+ }
2893
+ normalizeReasoningEffort(modelName, effort) {
2894
+ if (!effort) {
2895
+ return void 0;
2896
+ }
2897
+ if (effort === "none" && !allowsReasoningNone(modelName)) {
2898
+ return this.getDefaultReasoningEffortForModel(modelName);
2899
+ }
2900
+ if (effort === "minimal" && !allowsReasoningMinimal(modelName)) {
2901
+ return "none";
2902
+ }
2903
+ return effort;
2995
2904
  }
2996
2905
  };
2997
2906
 
2998
- // src/services/providers/zai/ZAIChatService.ts
2999
- var ZAIChatService = class {
2907
+ // src/services/providers/openrouter/OpenRouterChatService.ts
2908
+ var OpenRouterChatService = class {
3000
2909
  /**
3001
2910
  * Constructor
3002
- * @param apiKey Z.ai API key
2911
+ * @param apiKey OpenRouter API key
3003
2912
  * @param model Name of the model to use
3004
2913
  * @param visionModel Name of the vision model
2914
+ * @param tools Tool definitions (optional)
2915
+ * @param endpoint API endpoint (optional)
2916
+ * @param responseLength Response length configuration (optional)
2917
+ * @param appName Application name for OpenRouter analytics (optional)
2918
+ * @param appUrl Application URL for OpenRouter analytics (optional)
2919
+ * @param reasoning_effort Reasoning effort level (optional)
2920
+ * @param includeReasoning Whether to include reasoning in response (optional)
2921
+ * @param reasoningMaxTokens Maximum tokens for reasoning (optional)
3005
2922
  */
3006
- constructor(apiKey, model = MODEL_GLM_4_7, visionModel = MODEL_GLM_4_6V_FLASH, tools, endpoint = ENDPOINT_ZAI_CHAT_COMPLETIONS_API, responseLength, responseFormat, thinking) {
2923
+ 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
2924
  /** Provider name */
3008
- this.provider = "zai";
2925
+ this.provider = "openrouter";
2926
+ this.lastRequestTime = 0;
2927
+ this.requestCount = 0;
3009
2928
  this.apiKey = apiKey;
3010
2929
  this.model = model;
3011
2930
  this.tools = tools || [];
3012
2931
  this.endpoint = endpoint;
3013
2932
  this.responseLength = responseLength;
3014
- this.responseFormat = responseFormat;
3015
- this.thinking = thinking ?? { type: "disabled" };
2933
+ this.appName = appName;
2934
+ this.appUrl = appUrl;
2935
+ this.reasoning_effort = reasoning_effort;
2936
+ this.includeReasoning = includeReasoning;
2937
+ this.reasoningMaxTokens = reasoningMaxTokens;
3016
2938
  this.visionModel = visionModel;
3017
2939
  }
3018
2940
  /**
3019
2941
  * Get the current model name
2942
+ * @returns Model name
3020
2943
  */
3021
2944
  getModel() {
3022
2945
  return this.model;
3023
2946
  }
3024
2947
  /**
3025
2948
  * Get the current vision model name
2949
+ * @returns Vision model name
3026
2950
  */
3027
2951
  getVisionModel() {
3028
2952
  return this.visionModel;
3029
2953
  }
3030
2954
  /**
3031
- * Process chat messages
2955
+ * Apply rate limiting for free tier models
3032
2956
  */
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);
2957
+ async applyRateLimiting() {
2958
+ if (!isOpenRouterFreeModel(this.model)) {
3038
2959
  return;
3039
2960
  }
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;
2961
+ const now = Date.now();
2962
+ const timeSinceLastRequest = now - this.lastRequestTime;
2963
+ if (timeSinceLastRequest > 6e4) {
2964
+ this.requestCount = 0;
3045
2965
  }
3046
- throw new Error(
3047
- "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
3048
- );
2966
+ if (this.requestCount >= OPENROUTER_FREE_RATE_LIMIT_PER_MINUTE) {
2967
+ const waitTime = 6e4 - timeSinceLastRequest;
2968
+ if (waitTime > 0) {
2969
+ console.log(
2970
+ `Rate limit reached for free tier. Waiting ${waitTime}ms...`
2971
+ );
2972
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
2973
+ this.requestCount = 0;
2974
+ }
2975
+ }
2976
+ this.lastRequestTime = now;
2977
+ this.requestCount++;
2978
+ }
2979
+ /**
2980
+ * Process chat messages
2981
+ * @param messages Array of messages to send
2982
+ * @param onPartialResponse Callback to receive each part of streaming response
2983
+ * @param onCompleteResponse Callback to execute when response is complete
2984
+ */
2985
+ async processChat(messages, onPartialResponse, onCompleteResponse) {
2986
+ await this.applyRateLimiting();
2987
+ await processChatWithOptionalTools({
2988
+ hasTools: this.tools.length > 0,
2989
+ runWithoutTools: async () => {
2990
+ const res = await this.callOpenRouter(messages, this.model, true);
2991
+ return this.handleStream(res, onPartialResponse);
2992
+ },
2993
+ runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
2994
+ onCompleteResponse,
2995
+ toolErrorMessage: "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
2996
+ });
3049
2997
  }
3050
2998
  /**
3051
2999
  * Process chat messages with images
3000
+ * @param messages Array of messages to send (including images)
3001
+ * @param onPartialResponse Callback to receive each part of streaming response
3002
+ * @param onCompleteResponse Callback to execute when response is complete
3052
3003
  */
3053
3004
  async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
3054
- if (!isZaiVisionModel(this.visionModel)) {
3005
+ if (!isOpenRouterVisionModel(this.visionModel)) {
3055
3006
  throw new Error(
3056
3007
  `Model ${this.visionModel} does not support vision capabilities.`
3057
3008
  );
3058
3009
  }
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;
3010
+ await this.applyRateLimiting();
3011
+ try {
3012
+ await processChatWithOptionalTools({
3013
+ hasTools: this.tools.length > 0,
3014
+ runWithoutTools: async () => {
3015
+ const res = await this.callOpenRouter(
3016
+ messages,
3017
+ this.visionModel,
3018
+ true
3019
+ );
3020
+ return this.handleStream(res, onPartialResponse);
3021
+ },
3022
+ runWithTools: () => this.visionChatOnce(messages, true, onPartialResponse),
3023
+ onCompleteResponse,
3024
+ toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
3025
+ });
3026
+ } catch (error) {
3027
+ console.error("Error in processVisionChat:", error);
3028
+ throw error;
3074
3029
  }
3075
- throw new Error(
3076
- "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
3077
- );
3078
3030
  }
3079
3031
  /**
3080
3032
  * Process chat messages with tools (text only)
3033
+ * @param messages Array of messages to send
3034
+ * @param stream Whether to use streaming
3035
+ * @param onPartialResponse Callback for partial responses
3036
+ * @param maxTokens Maximum tokens for response (optional)
3037
+ * @returns Tool chat completion
3081
3038
  */
3082
3039
  async chatOnce(messages, stream = true, onPartialResponse = () => {
3083
3040
  }, maxTokens) {
3084
- const res = await this.callZAI(messages, this.model, stream, maxTokens);
3085
- return this.parseResponse(res, stream, onPartialResponse);
3041
+ await this.applyRateLimiting();
3042
+ const res = await this.callOpenRouter(
3043
+ messages,
3044
+ this.model,
3045
+ stream,
3046
+ maxTokens
3047
+ );
3048
+ return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
3086
3049
  }
3087
3050
  /**
3088
3051
  * Process vision chat messages with tools
3052
+ * @param messages Array of messages to send (including images)
3053
+ * @param stream Whether to use streaming
3054
+ * @param onPartialResponse Callback for partial responses
3055
+ * @param maxTokens Maximum tokens for response (optional)
3056
+ * @returns Tool chat completion
3089
3057
  */
3090
3058
  async visionChatOnce(messages, stream = false, onPartialResponse = () => {
3091
3059
  }, maxTokens) {
3092
- if (!isZaiVisionModel(this.visionModel)) {
3060
+ if (!isOpenRouterVisionModel(this.visionModel)) {
3093
3061
  throw new Error(
3094
3062
  `Model ${this.visionModel} does not support vision capabilities.`
3095
3063
  );
3096
3064
  }
3097
- const res = await this.callZAI(
3065
+ await this.applyRateLimiting();
3066
+ const res = await this.callOpenRouter(
3098
3067
  messages,
3099
3068
  this.visionModel,
3100
3069
  stream,
3101
3070
  maxTokens
3102
3071
  );
3103
- return this.parseResponse(res, stream, onPartialResponse);
3104
- }
3105
- async parseResponse(res, stream, onPartialResponse) {
3106
3072
  return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
3107
3073
  }
3108
- async callZAI(messages, model, stream = false, maxTokens) {
3074
+ /**
3075
+ * Call OpenRouter API
3076
+ */
3077
+ async callOpenRouter(messages, model, stream = false, maxTokens) {
3109
3078
  const body = this.buildRequestBody(messages, model, stream, maxTokens);
3110
- const res = await ChatServiceHttpClient.post(this.endpoint, body, {
3079
+ const headers = {
3111
3080
  Authorization: `Bearer ${this.apiKey}`
3112
- });
3081
+ };
3082
+ if (this.appUrl) {
3083
+ headers["HTTP-Referer"] = this.appUrl;
3084
+ }
3085
+ if (this.appName) {
3086
+ headers["X-Title"] = this.appName;
3087
+ }
3088
+ const res = await ChatServiceHttpClient.post(this.endpoint, body, headers);
3113
3089
  return res;
3114
3090
  }
3115
3091
  /**
3116
- * Build request body (OpenAI-compatible Chat Completions)
3092
+ * Build request body for OpenRouter API (OpenAI-compatible format)
3117
3093
  */
3118
3094
  buildRequestBody(messages, model, stream, maxTokens) {
3119
3095
  const body = {
3120
3096
  model,
3121
- stream,
3122
- messages
3097
+ messages,
3098
+ stream
3123
3099
  };
3124
3100
  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;
3101
+ if (tokenLimit) {
3102
+ console.warn(
3103
+ `OpenRouter: Token limits are not supported for gpt-oss-20b model due to known issues. Using unlimited tokens instead.`
3104
+ );
3130
3105
  }
3131
- if (this.thinking) {
3132
- body.thinking = this.thinking;
3106
+ if (this.reasoning_effort !== void 0 || this.includeReasoning !== void 0 || this.reasoningMaxTokens) {
3107
+ body.reasoning = {};
3108
+ if (this.reasoning_effort && this.reasoning_effort !== "none") {
3109
+ const effort = this.reasoning_effort === "minimal" ? "low" : this.reasoning_effort;
3110
+ body.reasoning.effort = effort;
3111
+ }
3112
+ if (this.reasoning_effort === "none" || this.includeReasoning !== true) {
3113
+ body.reasoning.exclude = true;
3114
+ }
3115
+ if (this.reasoningMaxTokens) {
3116
+ body.reasoning.max_tokens = this.reasoningMaxTokens;
3117
+ }
3118
+ } else {
3119
+ body.reasoning = { exclude: true };
3133
3120
  }
3134
- const tools = this.buildToolsDefinition();
3135
- if (tools.length > 0) {
3136
- body.tools = tools;
3121
+ if (this.tools.length > 0) {
3122
+ body.tools = buildOpenAICompatibleTools(this.tools, "chat-completions");
3137
3123
  body.tool_choice = "auto";
3138
- if (stream && isZaiToolStreamModel(model)) {
3139
- body.tool_stream = true;
3140
- }
3141
3124
  }
3142
3125
  return body;
3143
3126
  }
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
- }
3127
+ /**
3128
+ * Handle streaming response from OpenRouter
3129
+ * OpenRouter uses SSE format with potential comment lines
3130
+ */
3155
3131
  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;
3132
+ return parseOpenAICompatibleTextStream(res, onPartial, {
3133
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3134
+ });
3187
3135
  }
3188
3136
  /**
3189
3137
  * Parse streaming response with tool support
3190
3138
  */
3191
3139
  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
- };
3140
+ return parseOpenAICompatibleToolStream(res, onPartial, {
3141
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3142
+ });
3245
3143
  }
3246
3144
  /**
3247
3145
  * Parse non-streaming response
3248
3146
  */
3249
3147
  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
- };
3148
+ return parseOpenAICompatibleOneShot(data);
3268
3149
  }
3269
3150
  };
3270
3151
 
3271
- // src/services/providers/zai/ZAIChatServiceProvider.ts
3272
- var ZAIChatServiceProvider = class {
3152
+ // src/services/providers/openrouter/OpenRouterChatServiceProvider.ts
3153
+ var OpenRouterChatServiceProvider = class {
3273
3154
  /**
3274
3155
  * Create a chat service instance
3156
+ * @param options Service options
3157
+ * @returns OpenRouterChatService instance
3275
3158
  */
3276
3159
  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
- }
3160
+ const visionModel = resolveVisionModel({
3161
+ model: options.model,
3162
+ visionModel: options.visionModel,
3163
+ defaultModel: this.getDefaultModel(),
3164
+ defaultVisionModel: options.model || this.getDefaultModel(),
3165
+ supportsVisionForModel: (visionModel2) => this.supportsVisionForModel(visionModel2),
3166
+ validate: "explicit"
3167
+ });
3284
3168
  const tools = options.tools;
3285
- const thinking = options.thinking ?? { type: "disabled" };
3286
- return new ZAIChatService(
3169
+ const appName = options.appName;
3170
+ const appUrl = options.appUrl;
3171
+ return new OpenRouterChatService(
3287
3172
  options.apiKey,
3288
- model,
3173
+ options.model || this.getDefaultModel(),
3289
3174
  visionModel,
3290
3175
  tools,
3291
- options.endpoint || ENDPOINT_ZAI_CHAT_COMPLETIONS_API,
3176
+ options.endpoint,
3292
3177
  options.responseLength,
3293
- options.responseFormat,
3294
- thinking
3178
+ appName,
3179
+ appUrl,
3180
+ options.reasoning_effort,
3181
+ options.includeReasoning,
3182
+ options.reasoningMaxTokens
3295
3183
  );
3296
3184
  }
3297
3185
  /**
3298
3186
  * Get the provider name
3187
+ * @returns Provider name ('openrouter')
3299
3188
  */
3300
3189
  getProviderName() {
3301
- return "zai";
3190
+ return "openrouter";
3302
3191
  }
3303
3192
  /**
3304
3193
  * Get the list of supported models
3194
+ * Supports a curated list of OpenRouter models
3195
+ * @returns Array of supported model names
3305
3196
  */
3306
3197
  getSupportedModels() {
3307
3198
  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
3199
+ // Free models
3200
+ MODEL_GPT_OSS_20B_FREE,
3201
+ MODEL_ZAI_GLM_4_5_AIR_FREE,
3202
+ // OpenAI models
3203
+ MODEL_OPENAI_GPT_5_1_CHAT,
3204
+ MODEL_OPENAI_GPT_5_1_CODEX,
3205
+ MODEL_OPENAI_GPT_5_MINI,
3206
+ MODEL_OPENAI_GPT_5_NANO,
3207
+ MODEL_OPENAI_GPT_4O,
3208
+ MODEL_OPENAI_GPT_4_1_MINI,
3209
+ MODEL_OPENAI_GPT_4_1_NANO,
3210
+ // Anthropic models
3211
+ MODEL_ANTHROPIC_CLAUDE_OPUS_4,
3212
+ MODEL_ANTHROPIC_CLAUDE_SONNET_4,
3213
+ MODEL_ANTHROPIC_CLAUDE_3_7_SONNET,
3214
+ MODEL_ANTHROPIC_CLAUDE_3_5_SONNET,
3215
+ MODEL_ANTHROPIC_CLAUDE_4_5_HAIKU,
3216
+ // Gemini models
3217
+ MODEL_GOOGLE_GEMINI_2_5_PRO,
3218
+ MODEL_GOOGLE_GEMINI_2_5_FLASH,
3219
+ MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE_PREVIEW_09_2025,
3220
+ // Z.ai models
3221
+ MODEL_ZAI_GLM_4_7_FLASH,
3222
+ MODEL_ZAI_GLM_4_5_AIR,
3223
+ // Other models
3224
+ MODEL_MOONSHOTAI_KIMI_K2_5
3315
3225
  ];
3316
3226
  }
3317
3227
  /**
3318
3228
  * Get the default model
3229
+ * @returns Default model name (gpt-oss-20b:free)
3319
3230
  */
3320
3231
  getDefaultModel() {
3321
- return MODEL_GLM_4_7;
3232
+ return MODEL_GPT_OSS_20B_FREE;
3322
3233
  }
3323
3234
  /**
3324
- * Get the default vision model
3235
+ * Check if this provider supports vision (image processing)
3236
+ * @returns Vision support status (false - gpt-oss-20b does not support vision)
3325
3237
  */
3326
- getDefaultVisionModel() {
3327
- return MODEL_GLM_4_6V_FLASH;
3238
+ supportsVision() {
3239
+ return this.getSupportedModels().some(
3240
+ (model) => this.supportsVisionForModel(model)
3241
+ );
3328
3242
  }
3329
3243
  /**
3330
- * Check if this provider supports vision
3244
+ * Check if a specific model supports vision capabilities
3245
+ * @param model The model name to check
3246
+ * @returns True if the model supports vision, false otherwise
3331
3247
  */
3332
- supportsVision() {
3333
- return true;
3248
+ supportsVisionForModel(model) {
3249
+ return isOpenRouterVisionModel(model);
3334
3250
  }
3335
3251
  /**
3336
- * Check if a specific model supports vision capabilities
3252
+ * Get list of free tier models
3253
+ * @returns Array of free model names
3254
+ */
3255
+ getFreeModels() {
3256
+ return OPENROUTER_FREE_MODELS;
3257
+ }
3258
+ /**
3259
+ * Check if a model is free tier
3260
+ * @param model Model name to check
3261
+ * @returns True if the model is free
3337
3262
  */
3338
- supportsVisionForModel(model) {
3339
- return isZaiVisionModel(model);
3263
+ isModelFree(model) {
3264
+ return OPENROUTER_FREE_MODELS.includes(model) || model.endsWith(":free");
3340
3265
  }
3341
3266
  };
3342
3267
 
3343
- // src/services/providers/kimi/KimiChatService.ts
3344
- var KimiChatService = class {
3268
+ // src/services/providers/zai/ZAIChatService.ts
3269
+ var ZAIChatService = class {
3345
3270
  /**
3346
3271
  * Constructor
3347
- * @param apiKey Kimi API key
3272
+ * @param apiKey Z.ai API key
3348
3273
  * @param model Name of the model to use
3349
3274
  * @param visionModel Name of the vision model
3350
3275
  */
3351
- constructor(apiKey, model = MODEL_KIMI_K2_5, visionModel = MODEL_KIMI_K2_5, tools, endpoint = ENDPOINT_KIMI_CHAT_COMPLETIONS_API, responseLength, responseFormat, thinking) {
3276
+ constructor(apiKey, model = MODEL_GLM_4_7, visionModel = MODEL_GLM_4_6V_FLASH, tools, endpoint = ENDPOINT_ZAI_CHAT_COMPLETIONS_API, responseLength, responseFormat, thinking) {
3352
3277
  /** Provider name */
3353
- this.provider = "kimi";
3278
+ this.provider = "zai";
3354
3279
  this.apiKey = apiKey;
3355
3280
  this.model = model;
3356
3281
  this.tools = tools || [];
3357
3282
  this.endpoint = endpoint;
3358
3283
  this.responseLength = responseLength;
3359
3284
  this.responseFormat = responseFormat;
3360
- this.thinking = thinking ?? { type: "enabled" };
3285
+ this.thinking = thinking ?? { type: "disabled" };
3361
3286
  this.visionModel = visionModel;
3362
3287
  }
3363
3288
  /**
@@ -3376,57 +3301,43 @@ If it's in another language, summarize in that language.
3376
3301
  * Process chat messages
3377
3302
  */
3378
3303
  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
- );
3304
+ await processChatWithOptionalTools({
3305
+ hasTools: this.tools.length > 0,
3306
+ runWithoutTools: async () => {
3307
+ const res = await this.callZAI(messages, this.model, true);
3308
+ return this.handleStream(res, onPartialResponse);
3309
+ },
3310
+ runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
3311
+ onCompleteResponse,
3312
+ toolErrorMessage: "processChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
3313
+ });
3394
3314
  }
3395
3315
  /**
3396
3316
  * Process chat messages with images
3397
3317
  */
3398
3318
  async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
3399
- if (!isKimiVisionModel(this.visionModel)) {
3319
+ if (!isZaiVisionModel(this.visionModel)) {
3400
3320
  throw new Error(
3401
3321
  `Model ${this.visionModel} does not support vision capabilities.`
3402
3322
  );
3403
3323
  }
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
- );
3324
+ await processChatWithOptionalTools({
3325
+ hasTools: this.tools.length > 0,
3326
+ runWithoutTools: async () => {
3327
+ const res = await this.callZAI(messages, this.visionModel, true);
3328
+ return this.handleStream(res, onPartialResponse);
3329
+ },
3330
+ runWithTools: () => this.visionChatOnce(messages, true, onPartialResponse),
3331
+ onCompleteResponse,
3332
+ toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
3333
+ });
3423
3334
  }
3424
3335
  /**
3425
3336
  * Process chat messages with tools (text only)
3426
3337
  */
3427
3338
  async chatOnce(messages, stream = true, onPartialResponse = () => {
3428
3339
  }, maxTokens) {
3429
- const res = await this.callKimi(messages, this.model, stream, maxTokens);
3340
+ const res = await this.callZAI(messages, this.model, stream, maxTokens);
3430
3341
  return this.parseResponse(res, stream, onPartialResponse);
3431
3342
  }
3432
3343
  /**
@@ -3434,12 +3345,12 @@ If it's in another language, summarize in that language.
3434
3345
  */
3435
3346
  async visionChatOnce(messages, stream = false, onPartialResponse = () => {
3436
3347
  }, maxTokens) {
3437
- if (!isKimiVisionModel(this.visionModel)) {
3348
+ if (!isZaiVisionModel(this.visionModel)) {
3438
3349
  throw new Error(
3439
3350
  `Model ${this.visionModel} does not support vision capabilities.`
3440
3351
  );
3441
3352
  }
3442
- const res = await this.callKimi(
3353
+ const res = await this.callZAI(
3443
3354
  messages,
3444
3355
  this.visionModel,
3445
3356
  stream,
@@ -3450,7 +3361,7 @@ If it's in another language, summarize in that language.
3450
3361
  async parseResponse(res, stream, onPartialResponse) {
3451
3362
  return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
3452
3363
  }
3453
- async callKimi(messages, model, stream = false, maxTokens) {
3364
+ async callZAI(messages, model, stream = false, maxTokens) {
3454
3365
  const body = this.buildRequestBody(messages, model, stream, maxTokens);
3455
3366
  const res = await ChatServiceHttpClient.post(this.endpoint, body, {
3456
3367
  Authorization: `Bearer ${this.apiKey}`
@@ -3473,179 +3384,66 @@ If it's in another language, summarize in that language.
3473
3384
  if (this.responseFormat) {
3474
3385
  body.response_format = this.responseFormat;
3475
3386
  }
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
- }
3387
+ if (this.thinking) {
3388
+ body.thinking = this.thinking;
3485
3389
  }
3486
3390
  const tools = this.buildToolsDefinition();
3487
3391
  if (tools.length > 0) {
3488
3392
  body.tools = tools;
3489
3393
  body.tool_choice = "auto";
3394
+ if (stream && isZaiToolStreamModel(model)) {
3395
+ body.tool_stream = true;
3396
+ }
3490
3397
  }
3491
3398
  return body;
3492
3399
  }
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
3400
  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
- }));
3401
+ return buildOpenAICompatibleTools(this.tools, "chat-completions");
3509
3402
  }
3510
3403
  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;
3404
+ return parseOpenAICompatibleTextStream(res, onPartial, {
3405
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3406
+ });
3542
3407
  }
3543
3408
  /**
3544
3409
  * Parse streaming response with tool support
3545
3410
  */
3546
3411
  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
- };
3412
+ return parseOpenAICompatibleToolStream(res, onPartial, {
3413
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3414
+ });
3600
3415
  }
3601
3416
  /**
3602
3417
  * Parse non-streaming response
3603
3418
  */
3604
3419
  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
- };
3420
+ return parseOpenAICompatibleOneShot(data);
3623
3421
  }
3624
3422
  };
3625
3423
 
3626
- // src/services/providers/kimi/KimiChatServiceProvider.ts
3627
- var KimiChatServiceProvider = class {
3424
+ // src/services/providers/zai/ZAIChatServiceProvider.ts
3425
+ var ZAIChatServiceProvider = class {
3628
3426
  /**
3629
3427
  * Create a chat service instance
3630
3428
  */
3631
3429
  createChatService(options) {
3632
- const endpoint = this.resolveEndpoint(options);
3633
3430
  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
- }
3431
+ const visionModel = resolveVisionModel({
3432
+ model,
3433
+ visionModel: options.visionModel,
3434
+ defaultModel: this.getDefaultModel(),
3435
+ defaultVisionModel: this.getDefaultVisionModel(),
3436
+ supportsVisionForModel: (visionModel2) => this.supportsVisionForModel(visionModel2),
3437
+ validate: "explicit"
3438
+ });
3640
3439
  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(
3440
+ const thinking = options.thinking ?? { type: "disabled" };
3441
+ return new ZAIChatService(
3644
3442
  options.apiKey,
3645
3443
  model,
3646
3444
  visionModel,
3647
3445
  tools,
3648
- endpoint,
3446
+ options.endpoint || ENDPOINT_ZAI_CHAT_COMPLETIONS_API,
3649
3447
  options.responseLength,
3650
3448
  options.responseFormat,
3651
3449
  thinking
@@ -3655,25 +3453,33 @@ If it's in another language, summarize in that language.
3655
3453
  * Get the provider name
3656
3454
  */
3657
3455
  getProviderName() {
3658
- return "kimi";
3456
+ return "zai";
3659
3457
  }
3660
3458
  /**
3661
3459
  * Get the list of supported models
3662
3460
  */
3663
3461
  getSupportedModels() {
3664
- return [MODEL_KIMI_K2_5];
3462
+ return [
3463
+ MODEL_GLM_4_7,
3464
+ MODEL_GLM_4_7_FLASHX,
3465
+ MODEL_GLM_4_7_FLASH,
3466
+ MODEL_GLM_4_6,
3467
+ MODEL_GLM_4_6V,
3468
+ MODEL_GLM_4_6V_FLASHX,
3469
+ MODEL_GLM_4_6V_FLASH
3470
+ ];
3665
3471
  }
3666
3472
  /**
3667
3473
  * Get the default model
3668
3474
  */
3669
3475
  getDefaultModel() {
3670
- return MODEL_KIMI_K2_5;
3476
+ return MODEL_GLM_4_7;
3671
3477
  }
3672
3478
  /**
3673
3479
  * Get the default vision model
3674
3480
  */
3675
3481
  getDefaultVisionModel() {
3676
- return MODEL_KIMI_K2_5;
3482
+ return MODEL_GLM_4_6V_FLASH;
3677
3483
  }
3678
3484
  /**
3679
3485
  * Check if this provider supports vision
@@ -3685,26 +3491,20 @@ If it's in another language, summarize in that language.
3685
3491
  * Check if a specific model supports vision capabilities
3686
3492
  */
3687
3493
  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(/\/+$/, "");
3494
+ return isZaiVisionModel(model);
3705
3495
  }
3706
3496
  };
3707
3497
 
3498
+ // src/services/providers/index.ts
3499
+ var DEFAULT_CHAT_SERVICE_PROVIDERS = [
3500
+ new OpenAIChatServiceProvider(),
3501
+ new GeminiChatServiceProvider(),
3502
+ new ClaudeChatServiceProvider(),
3503
+ new OpenRouterChatServiceProvider(),
3504
+ new ZAIChatServiceProvider(),
3505
+ new KimiChatServiceProvider()
3506
+ ];
3507
+
3708
3508
  // src/services/ChatServiceFactory.ts
3709
3509
  var ChatServiceFactory = class {
3710
3510
  /**
@@ -3714,12 +3514,6 @@ If it's in another language, summarize in that language.
3714
3514
  static registerProvider(provider) {
3715
3515
  this.providers.set(provider.getProviderName(), provider);
3716
3516
  }
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
3517
  static createChatService(providerName, options) {
3724
3518
  const provider = this.providers.get(providerName);
3725
3519
  if (!provider) {
@@ -3753,89 +3547,9 @@ If it's in another language, summarize in that language.
3753
3547
  };
3754
3548
  /** Map of registered providers */
3755
3549
  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
- }
3550
+ DEFAULT_CHAT_SERVICE_PROVIDERS.forEach(
3551
+ (provider) => ChatServiceFactory.registerProvider(provider)
3552
+ );
3839
3553
 
3840
3554
  // src/adapters/gasFetch.ts
3841
3555
  function installGASFetch() {