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