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