@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.
- package/README.ja.md +2 -2
- package/README.md +2 -2
- package/dist/cjs/constants/claude.d.ts +1 -0
- package/dist/cjs/constants/claude.d.ts.map +1 -1
- package/dist/cjs/constants/claude.js +3 -1
- package/dist/cjs/constants/claude.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/services/ChatServiceFactory.d.ts +2 -2
- package/dist/cjs/services/ChatServiceFactory.d.ts.map +1 -1
- package/dist/cjs/services/ChatServiceFactory.js +2 -24
- package/dist/cjs/services/ChatServiceFactory.js.map +1 -1
- package/dist/cjs/services/providers/ChatServiceProvider.d.ts +35 -5
- package/dist/cjs/services/providers/ChatServiceProvider.d.ts.map +1 -1
- package/dist/cjs/services/providers/claude/ClaudeChatService.d.ts.map +1 -1
- package/dist/cjs/services/providers/claude/ClaudeChatService.js +21 -36
- package/dist/cjs/services/providers/claude/ClaudeChatService.js.map +1 -1
- package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.d.ts +3 -3
- package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.d.ts.map +1 -1
- package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.js +10 -4
- package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.js.map +1 -1
- package/dist/cjs/services/providers/gemini/GeminiChatService.d.ts.map +1 -1
- package/dist/cjs/services/providers/gemini/GeminiChatService.js +28 -45
- package/dist/cjs/services/providers/gemini/GeminiChatService.js.map +1 -1
- package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.d.ts +3 -3
- package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.d.ts.map +1 -1
- package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.js +9 -4
- package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.js.map +1 -1
- package/dist/cjs/services/providers/index.d.ts +8 -0
- package/dist/cjs/services/providers/index.d.ts.map +1 -0
- package/dist/cjs/services/providers/index.js +18 -0
- package/dist/cjs/services/providers/index.js.map +1 -0
- package/dist/cjs/services/providers/kimi/KimiChatService.d.ts.map +1 -1
- package/dist/cjs/services/providers/kimi/KimiChatService.js +31 -162
- package/dist/cjs/services/providers/kimi/KimiChatService.js.map +1 -1
- package/dist/cjs/services/providers/kimi/KimiChatServiceProvider.d.ts +3 -3
- package/dist/cjs/services/providers/kimi/KimiChatServiceProvider.d.ts.map +1 -1
- package/dist/cjs/services/providers/kimi/KimiChatServiceProvider.js +9 -8
- package/dist/cjs/services/providers/kimi/KimiChatServiceProvider.js.map +1 -1
- package/dist/cjs/services/providers/openai/OpenAIChatService.d.ts.map +1 -1
- package/dist/cjs/services/providers/openai/OpenAIChatService.js +47 -199
- package/dist/cjs/services/providers/openai/OpenAIChatService.js.map +1 -1
- package/dist/cjs/services/providers/openai/OpenAIChatServiceProvider.d.ts +3 -3
- package/dist/cjs/services/providers/openai/OpenAIChatServiceProvider.d.ts.map +1 -1
- package/dist/cjs/services/providers/openai/OpenAIChatServiceProvider.js +9 -4
- package/dist/cjs/services/providers/openai/OpenAIChatServiceProvider.js.map +1 -1
- package/dist/cjs/services/providers/openrouter/OpenRouterChatService.d.ts.map +1 -1
- package/dist/cjs/services/providers/openrouter/OpenRouterChatService.js +31 -165
- package/dist/cjs/services/providers/openrouter/OpenRouterChatService.js.map +1 -1
- package/dist/cjs/services/providers/openrouter/OpenRouterChatServiceProvider.d.ts +3 -3
- package/dist/cjs/services/providers/openrouter/OpenRouterChatServiceProvider.d.ts.map +1 -1
- package/dist/cjs/services/providers/openrouter/OpenRouterChatServiceProvider.js +9 -6
- package/dist/cjs/services/providers/openrouter/OpenRouterChatServiceProvider.js.map +1 -1
- package/dist/cjs/services/providers/zai/ZAIChatService.d.ts.map +1 -1
- package/dist/cjs/services/providers/zai/ZAIChatService.js +31 -162
- package/dist/cjs/services/providers/zai/ZAIChatService.js.map +1 -1
- package/dist/cjs/services/providers/zai/ZAIChatServiceProvider.d.ts +3 -3
- package/dist/cjs/services/providers/zai/ZAIChatServiceProvider.d.ts.map +1 -1
- package/dist/cjs/services/providers/zai/ZAIChatServiceProvider.js +9 -8
- package/dist/cjs/services/providers/zai/ZAIChatServiceProvider.js.map +1 -1
- package/dist/cjs/utils/index.d.ts +4 -0
- package/dist/cjs/utils/index.d.ts.map +1 -1
- package/dist/cjs/utils/index.js +4 -0
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/openaiCompatibleSse.d.ts +10 -0
- package/dist/cjs/utils/openaiCompatibleSse.d.ts.map +1 -0
- package/dist/cjs/utils/openaiCompatibleSse.js +124 -0
- package/dist/cjs/utils/openaiCompatibleSse.js.map +1 -0
- package/dist/cjs/utils/openaiCompatibleTools.d.ts +4 -0
- package/dist/cjs/utils/openaiCompatibleTools.d.ts.map +1 -0
- package/dist/cjs/utils/openaiCompatibleTools.js +25 -0
- package/dist/cjs/utils/openaiCompatibleTools.js.map +1 -0
- package/dist/cjs/utils/processChatFlow.d.ts +12 -0
- package/dist/cjs/utils/processChatFlow.d.ts.map +1 -0
- package/dist/cjs/utils/processChatFlow.js +22 -0
- package/dist/cjs/utils/processChatFlow.js.map +1 -0
- package/dist/cjs/utils/visionModelResolver.d.ts +12 -0
- package/dist/cjs/utils/visionModelResolver.d.ts.map +1 -0
- package/dist/cjs/utils/visionModelResolver.js +22 -0
- package/dist/cjs/utils/visionModelResolver.js.map +1 -0
- package/dist/esm/constants/claude.d.ts +1 -0
- package/dist/esm/constants/claude.d.ts.map +1 -1
- package/dist/esm/constants/claude.js +2 -0
- package/dist/esm/constants/claude.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/services/ChatServiceFactory.d.ts +2 -2
- package/dist/esm/services/ChatServiceFactory.d.ts.map +1 -1
- package/dist/esm/services/ChatServiceFactory.js +2 -24
- package/dist/esm/services/ChatServiceFactory.js.map +1 -1
- package/dist/esm/services/providers/ChatServiceProvider.d.ts +35 -5
- package/dist/esm/services/providers/ChatServiceProvider.d.ts.map +1 -1
- package/dist/esm/services/providers/claude/ClaudeChatService.d.ts.map +1 -1
- package/dist/esm/services/providers/claude/ClaudeChatService.js +21 -36
- package/dist/esm/services/providers/claude/ClaudeChatService.js.map +1 -1
- package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.d.ts +3 -3
- package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.d.ts.map +1 -1
- package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.js +11 -5
- package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.js.map +1 -1
- package/dist/esm/services/providers/gemini/GeminiChatService.d.ts.map +1 -1
- package/dist/esm/services/providers/gemini/GeminiChatService.js +28 -45
- package/dist/esm/services/providers/gemini/GeminiChatService.js.map +1 -1
- package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.d.ts +3 -3
- package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.d.ts.map +1 -1
- package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.js +9 -4
- package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.js.map +1 -1
- package/dist/esm/services/providers/index.d.ts +8 -0
- package/dist/esm/services/providers/index.d.ts.map +1 -0
- package/dist/esm/services/providers/index.js +15 -0
- package/dist/esm/services/providers/index.js.map +1 -0
- package/dist/esm/services/providers/kimi/KimiChatService.d.ts.map +1 -1
- package/dist/esm/services/providers/kimi/KimiChatService.js +31 -162
- package/dist/esm/services/providers/kimi/KimiChatService.js.map +1 -1
- package/dist/esm/services/providers/kimi/KimiChatServiceProvider.d.ts +3 -3
- package/dist/esm/services/providers/kimi/KimiChatServiceProvider.d.ts.map +1 -1
- package/dist/esm/services/providers/kimi/KimiChatServiceProvider.js +9 -8
- package/dist/esm/services/providers/kimi/KimiChatServiceProvider.js.map +1 -1
- package/dist/esm/services/providers/openai/OpenAIChatService.d.ts.map +1 -1
- package/dist/esm/services/providers/openai/OpenAIChatService.js +47 -199
- package/dist/esm/services/providers/openai/OpenAIChatService.js.map +1 -1
- package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.d.ts +3 -3
- package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.d.ts.map +1 -1
- package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.js +9 -4
- package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.js.map +1 -1
- package/dist/esm/services/providers/openrouter/OpenRouterChatService.d.ts.map +1 -1
- package/dist/esm/services/providers/openrouter/OpenRouterChatService.js +31 -165
- package/dist/esm/services/providers/openrouter/OpenRouterChatService.js.map +1 -1
- package/dist/esm/services/providers/openrouter/OpenRouterChatServiceProvider.d.ts +3 -3
- package/dist/esm/services/providers/openrouter/OpenRouterChatServiceProvider.d.ts.map +1 -1
- package/dist/esm/services/providers/openrouter/OpenRouterChatServiceProvider.js +9 -6
- package/dist/esm/services/providers/openrouter/OpenRouterChatServiceProvider.js.map +1 -1
- package/dist/esm/services/providers/zai/ZAIChatService.d.ts.map +1 -1
- package/dist/esm/services/providers/zai/ZAIChatService.js +31 -162
- package/dist/esm/services/providers/zai/ZAIChatService.js.map +1 -1
- package/dist/esm/services/providers/zai/ZAIChatServiceProvider.d.ts +3 -3
- package/dist/esm/services/providers/zai/ZAIChatServiceProvider.d.ts.map +1 -1
- package/dist/esm/services/providers/zai/ZAIChatServiceProvider.js +9 -8
- package/dist/esm/services/providers/zai/ZAIChatServiceProvider.js.map +1 -1
- package/dist/esm/utils/index.d.ts +4 -0
- package/dist/esm/utils/index.d.ts.map +1 -1
- package/dist/esm/utils/index.js +4 -0
- package/dist/esm/utils/index.js.map +1 -1
- package/dist/esm/utils/openaiCompatibleSse.d.ts +10 -0
- package/dist/esm/utils/openaiCompatibleSse.d.ts.map +1 -0
- package/dist/esm/utils/openaiCompatibleSse.js +119 -0
- package/dist/esm/utils/openaiCompatibleSse.js.map +1 -0
- package/dist/esm/utils/openaiCompatibleTools.d.ts +4 -0
- package/dist/esm/utils/openaiCompatibleTools.d.ts.map +1 -0
- package/dist/esm/utils/openaiCompatibleTools.js +21 -0
- package/dist/esm/utils/openaiCompatibleTools.js.map +1 -0
- package/dist/esm/utils/processChatFlow.d.ts +12 -0
- package/dist/esm/utils/processChatFlow.d.ts.map +1 -0
- package/dist/esm/utils/processChatFlow.js +19 -0
- package/dist/esm/utils/processChatFlow.js.map +1 -0
- package/dist/esm/utils/visionModelResolver.d.ts +12 -0
- package/dist/esm/utils/visionModelResolver.d.ts.map +1 -0
- package/dist/esm/utils/visionModelResolver.js +18 -0
- package/dist/esm/utils/visionModelResolver.js.map +1 -0
- package/dist/umd/aituber-onair-chat.js +1586 -1872
- package/dist/umd/aituber-onair-chat.min.js +6 -15
- 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/
|
|
532
|
-
var
|
|
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
|
|
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 =
|
|
794
|
+
constructor(apiKey, model = MODEL_CLAUDE_3_HAIKU, visionModel = MODEL_CLAUDE_3_HAIKU, tools = [], mcpServers = [], responseLength) {
|
|
540
795
|
/** Provider name */
|
|
541
|
-
this.provider = "
|
|
796
|
+
this.provider = "claude";
|
|
542
797
|
this.apiKey = apiKey;
|
|
543
|
-
this.model = model;
|
|
544
|
-
this.
|
|
545
|
-
this.
|
|
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.
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
*
|
|
890
|
+
* Convert AITuber OnAir messages to Claude format
|
|
891
|
+
* @param messages Array of messages
|
|
892
|
+
* @returns Claude formatted messages
|
|
768
893
|
*/
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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: "
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
804
|
-
|
|
945
|
+
}).filter((b) => b);
|
|
946
|
+
return {
|
|
947
|
+
role: this.mapRoleToClaude(msg.role),
|
|
948
|
+
content
|
|
949
|
+
};
|
|
805
950
|
}
|
|
806
|
-
return
|
|
951
|
+
return {
|
|
952
|
+
role: this.mapRoleToClaude(msg.role),
|
|
953
|
+
content: []
|
|
954
|
+
};
|
|
807
955
|
});
|
|
808
956
|
}
|
|
809
957
|
/**
|
|
810
|
-
*
|
|
958
|
+
* Map AITuber OnAir roles to Claude roles
|
|
959
|
+
* @param role AITuber OnAir role
|
|
960
|
+
* @returns Claude role
|
|
811
961
|
*/
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
-
*
|
|
975
|
+
* Get MIME type from URL
|
|
976
|
+
* @param url Image URL
|
|
977
|
+
* @returns MIME type
|
|
845
978
|
*/
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
907
|
-
while ((
|
|
908
|
-
const
|
|
909
|
-
buf = buf.slice(
|
|
910
|
-
if (!
|
|
911
|
-
const payload =
|
|
912
|
-
if (payload === "[DONE]")
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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 (
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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:
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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.
|
|
956
|
-
input:
|
|
957
|
-
})
|
|
958
|
-
)
|
|
959
|
-
|
|
960
|
-
|
|
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:
|
|
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
|
-
*
|
|
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
|
|
971
|
-
|
|
972
|
-
const
|
|
973
|
-
const
|
|
974
|
-
|
|
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
|
-
*
|
|
1026
|
-
* @
|
|
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
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
-
*
|
|
1216
|
+
* Convert internal completion to standard ToolChatCompletion
|
|
1217
|
+
* @param completion Internal completion result
|
|
1218
|
+
* @returns Standard ToolChatCompletion
|
|
1084
1219
|
*/
|
|
1085
|
-
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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:
|
|
1227
|
+
blocks: standardBlocks,
|
|
1228
|
+
stop_reason: completion.stop_reason
|
|
1109
1229
|
};
|
|
1110
1230
|
}
|
|
1111
1231
|
};
|
|
1112
1232
|
|
|
1113
|
-
// src/services/providers/
|
|
1114
|
-
var
|
|
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
|
|
1237
|
+
* @param options Service options (can include mcpServers)
|
|
1238
|
+
* @returns ClaudeChatService instance
|
|
1119
1239
|
*/
|
|
1120
1240
|
createChatService(options) {
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
-
|
|
1142
|
-
|
|
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 ('
|
|
1260
|
+
* @returns Provider name ('claude')
|
|
1152
1261
|
*/
|
|
1153
1262
|
getProviderName() {
|
|
1154
|
-
return "
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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 =
|
|
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/
|
|
2002
|
-
var
|
|
2056
|
+
// src/services/providers/kimi/KimiChatService.ts
|
|
2057
|
+
var KimiChatService = class {
|
|
2003
2058
|
/**
|
|
2004
2059
|
* Constructor
|
|
2005
|
-
* @param apiKey
|
|
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 =
|
|
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 = "
|
|
2066
|
+
this.provider = "kimi";
|
|
2015
2067
|
this.apiKey = apiKey;
|
|
2016
|
-
this.model = model
|
|
2017
|
-
this.
|
|
2018
|
-
this.
|
|
2019
|
-
this.mcpServers = mcpServers;
|
|
2068
|
+
this.model = model;
|
|
2069
|
+
this.tools = tools || [];
|
|
2070
|
+
this.endpoint = endpoint;
|
|
2020
2071
|
this.responseLength = responseLength;
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
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
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
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.
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
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
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
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
|
-
*
|
|
2132
|
-
* @param messages Array of vision messages
|
|
2133
|
-
* @returns Claude formatted vision messages
|
|
2124
|
+
* Process chat messages with tools (text only)
|
|
2134
2125
|
*/
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
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
|
-
*
|
|
2187
|
-
* @param role AITuber OnAir role
|
|
2188
|
-
* @returns Claude role
|
|
2132
|
+
* Process vision chat messages with tools
|
|
2189
2133
|
*/
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
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
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
2166
|
+
messages
|
|
2247
2167
|
};
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
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
|
-
|
|
2260
|
-
|
|
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
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
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
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
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
|
-
|
|
2365
|
-
|
|
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
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
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
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
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
|
-
*
|
|
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
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
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
|
-
*
|
|
2445
|
-
* @param completion Internal completion result
|
|
2446
|
-
* @returns Standard ToolChatCompletion
|
|
2215
|
+
* Parse non-streaming response
|
|
2447
2216
|
*/
|
|
2448
|
-
|
|
2449
|
-
|
|
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/
|
|
2462
|
-
var
|
|
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
|
|
2470
|
-
|
|
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
|
-
|
|
2243
|
+
model,
|
|
2473
2244
|
visionModel,
|
|
2474
|
-
|
|
2475
|
-
|
|
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 "
|
|
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
|
|
2268
|
+
return MODEL_KIMI_K2_5;
|
|
2509
2269
|
}
|
|
2510
2270
|
/**
|
|
2511
|
-
*
|
|
2512
|
-
|
|
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
|
|
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/
|
|
2528
|
-
var
|
|
2306
|
+
// src/services/providers/openai/OpenAIChatService.ts
|
|
2307
|
+
var OpenAIChatService = class {
|
|
2529
2308
|
/**
|
|
2530
2309
|
* Constructor
|
|
2531
|
-
* @param apiKey
|
|
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 =
|
|
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 = "
|
|
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.
|
|
2554
|
-
this.appUrl = appUrl;
|
|
2323
|
+
this.verbosity = verbosity;
|
|
2555
2324
|
this.reasoning_effort = reasoning_effort;
|
|
2556
|
-
this.
|
|
2557
|
-
|
|
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
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
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
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
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.
|
|
2672
|
-
|
|
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
|
-
|
|
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
|
|
2447
|
+
return this.parseResponse(res, stream, onPartialResponse);
|
|
2703
2448
|
}
|
|
2704
2449
|
/**
|
|
2705
|
-
*
|
|
2450
|
+
* Parse response based on endpoint type
|
|
2706
2451
|
*/
|
|
2707
|
-
async
|
|
2708
|
-
const
|
|
2709
|
-
|
|
2710
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2477
|
+
if (isResponsesAPI) {
|
|
2478
|
+
body.max_output_tokens = tokenLimit;
|
|
2479
|
+
} else {
|
|
2480
|
+
body.max_completion_tokens = tokenLimit;
|
|
2735
2481
|
}
|
|
2736
|
-
if (
|
|
2737
|
-
body.
|
|
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.
|
|
2485
|
+
body.messages = messages;
|
|
2750
2486
|
}
|
|
2751
|
-
if (
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
2766
|
-
* OpenRouter uses SSE format with potential comment lines
|
|
2524
|
+
* Validate MCP servers compatibility with the current endpoint
|
|
2767
2525
|
*/
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
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
|
-
|
|
2795
|
-
|
|
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
|
|
2583
|
+
return toolDefs;
|
|
2800
2584
|
}
|
|
2801
2585
|
/**
|
|
2802
|
-
*
|
|
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 (
|
|
2817
|
-
const
|
|
2818
|
-
if (
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
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
|
-
|
|
2843
|
-
|
|
2658
|
+
eventType = "";
|
|
2659
|
+
eventData = "";
|
|
2844
2660
|
}
|
|
2845
2661
|
}
|
|
2846
2662
|
}
|
|
2847
|
-
const toolBlocks = Array.from(toolCallsMap.
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
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
|
-
|
|
2863
|
-
const choice = data.choices?.[0];
|
|
2738
|
+
parseResponsesOneShot(data) {
|
|
2864
2739
|
const blocks = [];
|
|
2865
|
-
if (
|
|
2866
|
-
|
|
2867
|
-
(
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
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:
|
|
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/
|
|
2885
|
-
var
|
|
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
|
|
2771
|
+
* @returns OpenAIChatService instance
|
|
2890
2772
|
*/
|
|
2891
2773
|
createChatService(options) {
|
|
2892
|
-
const
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
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
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
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
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
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 ('
|
|
2809
|
+
* @returns Provider name ('openai')
|
|
2918
2810
|
*/
|
|
2919
2811
|
getProviderName() {
|
|
2920
|
-
return "
|
|
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
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
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
|
|
2837
|
+
* @returns Default model name
|
|
2960
2838
|
*/
|
|
2961
2839
|
getDefaultModel() {
|
|
2962
|
-
return
|
|
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 (
|
|
2844
|
+
* @returns Vision support status (true)
|
|
2967
2845
|
*/
|
|
2968
2846
|
supportsVision() {
|
|
2969
|
-
return
|
|
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
|
|
2855
|
+
return VISION_SUPPORTED_MODELS.includes(model);
|
|
2980
2856
|
}
|
|
2981
2857
|
/**
|
|
2982
|
-
*
|
|
2983
|
-
* @
|
|
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
|
-
|
|
2986
|
-
|
|
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
|
-
*
|
|
2990
|
-
*
|
|
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
|
-
|
|
2994
|
-
|
|
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/
|
|
2999
|
-
var
|
|
2907
|
+
// src/services/providers/openrouter/OpenRouterChatService.ts
|
|
2908
|
+
var OpenRouterChatService = class {
|
|
3000
2909
|
/**
|
|
3001
2910
|
* Constructor
|
|
3002
|
-
* @param apiKey
|
|
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 =
|
|
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 = "
|
|
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.
|
|
3015
|
-
this.
|
|
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
|
-
*
|
|
2955
|
+
* Apply rate limiting for free tier models
|
|
3032
2956
|
*/
|
|
3033
|
-
async
|
|
3034
|
-
if (this.
|
|
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
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
return;
|
|
2961
|
+
const now = Date.now();
|
|
2962
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
2963
|
+
if (timeSinceLastRequest > 6e4) {
|
|
2964
|
+
this.requestCount = 0;
|
|
3045
2965
|
}
|
|
3046
|
-
|
|
3047
|
-
|
|
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 (!
|
|
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
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
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
|
-
|
|
3085
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
3122
|
-
|
|
3097
|
+
messages,
|
|
3098
|
+
stream
|
|
3123
3099
|
};
|
|
3124
3100
|
const tokenLimit = maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength);
|
|
3125
|
-
if (tokenLimit
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
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.
|
|
3132
|
-
body.
|
|
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
|
-
|
|
3135
|
-
|
|
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
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
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
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
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
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
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
|
-
|
|
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/
|
|
3272
|
-
var
|
|
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
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
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
|
|
3286
|
-
|
|
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
|
|
3176
|
+
options.endpoint,
|
|
3292
3177
|
options.responseLength,
|
|
3293
|
-
|
|
3294
|
-
|
|
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 "
|
|
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
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
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
|
|
3232
|
+
return MODEL_GPT_OSS_20B_FREE;
|
|
3322
3233
|
}
|
|
3323
3234
|
/**
|
|
3324
|
-
*
|
|
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
|
-
|
|
3327
|
-
return
|
|
3238
|
+
supportsVision() {
|
|
3239
|
+
return this.getSupportedModels().some(
|
|
3240
|
+
(model) => this.supportsVisionForModel(model)
|
|
3241
|
+
);
|
|
3328
3242
|
}
|
|
3329
3243
|
/**
|
|
3330
|
-
* Check if
|
|
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
|
-
|
|
3333
|
-
return
|
|
3248
|
+
supportsVisionForModel(model) {
|
|
3249
|
+
return isOpenRouterVisionModel(model);
|
|
3334
3250
|
}
|
|
3335
3251
|
/**
|
|
3336
|
-
*
|
|
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
|
-
|
|
3339
|
-
return
|
|
3263
|
+
isModelFree(model) {
|
|
3264
|
+
return OPENROUTER_FREE_MODELS.includes(model) || model.endsWith(":free");
|
|
3340
3265
|
}
|
|
3341
3266
|
};
|
|
3342
3267
|
|
|
3343
|
-
// src/services/providers/
|
|
3344
|
-
var
|
|
3268
|
+
// src/services/providers/zai/ZAIChatService.ts
|
|
3269
|
+
var ZAIChatService = class {
|
|
3345
3270
|
/**
|
|
3346
3271
|
* Constructor
|
|
3347
|
-
* @param apiKey
|
|
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 =
|
|
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 = "
|
|
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: "
|
|
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
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
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 (!
|
|
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
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
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.
|
|
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 (!
|
|
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.
|
|
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
|
|
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
|
-
|
|
3477
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
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
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
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
|
-
|
|
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/
|
|
3627
|
-
var
|
|
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 =
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
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
|
|
3642
|
-
|
|
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 "
|
|
3456
|
+
return "zai";
|
|
3659
3457
|
}
|
|
3660
3458
|
/**
|
|
3661
3459
|
* Get the list of supported models
|
|
3662
3460
|
*/
|
|
3663
3461
|
getSupportedModels() {
|
|
3664
|
-
return [
|
|
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
|
|
3476
|
+
return MODEL_GLM_4_7;
|
|
3671
3477
|
}
|
|
3672
3478
|
/**
|
|
3673
3479
|
* Get the default vision model
|
|
3674
3480
|
*/
|
|
3675
3481
|
getDefaultVisionModel() {
|
|
3676
|
-
return
|
|
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
|
|
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
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
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() {
|