@aituber-onair/chat 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.ja.md +54 -2
  2. package/README.md +54 -2
  3. package/dist/cjs/constants/deepseek.d.ts +11 -0
  4. package/dist/cjs/constants/deepseek.d.ts.map +1 -0
  5. package/dist/cjs/constants/deepseek.js +22 -0
  6. package/dist/cjs/constants/deepseek.js.map +1 -0
  7. package/dist/cjs/constants/index.d.ts +2 -0
  8. package/dist/cjs/constants/index.d.ts.map +1 -1
  9. package/dist/cjs/constants/index.js +2 -0
  10. package/dist/cjs/constants/index.js.map +1 -1
  11. package/dist/cjs/constants/mistral.d.ts +16 -0
  12. package/dist/cjs/constants/mistral.d.ts.map +1 -0
  13. package/dist/cjs/constants/mistral.js +45 -0
  14. package/dist/cjs/constants/mistral.js.map +1 -0
  15. package/dist/cjs/index.d.ts +5 -1
  16. package/dist/cjs/index.d.ts.map +1 -1
  17. package/dist/cjs/index.js +11 -1
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/services/providers/ChatServiceProvider.d.ts +9 -1
  20. package/dist/cjs/services/providers/ChatServiceProvider.d.ts.map +1 -1
  21. package/dist/cjs/services/providers/deepseek/DeepSeekChatService.d.ts +7 -0
  22. package/dist/cjs/services/providers/deepseek/DeepSeekChatService.d.ts.map +1 -0
  23. package/dist/cjs/services/providers/deepseek/DeepSeekChatService.js +12 -0
  24. package/dist/cjs/services/providers/deepseek/DeepSeekChatService.js.map +1 -0
  25. package/dist/cjs/services/providers/deepseek/DeepSeekChatServiceProvider.d.ts +16 -0
  26. package/dist/cjs/services/providers/deepseek/DeepSeekChatServiceProvider.d.ts.map +1 -0
  27. package/dist/cjs/services/providers/deepseek/DeepSeekChatServiceProvider.js +57 -0
  28. package/dist/cjs/services/providers/deepseek/DeepSeekChatServiceProvider.js.map +1 -0
  29. package/dist/cjs/services/providers/index.d.ts +3 -1
  30. package/dist/cjs/services/providers/index.d.ts.map +1 -1
  31. package/dist/cjs/services/providers/index.js +4 -0
  32. package/dist/cjs/services/providers/index.js.map +1 -1
  33. package/dist/cjs/services/providers/mistral/MistralChatService.d.ts +8 -0
  34. package/dist/cjs/services/providers/mistral/MistralChatService.d.ts.map +1 -0
  35. package/dist/cjs/services/providers/mistral/MistralChatService.js +12 -0
  36. package/dist/cjs/services/providers/mistral/MistralChatService.js.map +1 -0
  37. package/dist/cjs/services/providers/mistral/MistralChatServiceProvider.d.ts +17 -0
  38. package/dist/cjs/services/providers/mistral/MistralChatServiceProvider.d.ts.map +1 -0
  39. package/dist/cjs/services/providers/mistral/MistralChatServiceProvider.js +72 -0
  40. package/dist/cjs/services/providers/mistral/MistralChatServiceProvider.js.map +1 -0
  41. package/dist/cjs/services/providers/openai/OpenAIChatService.d.ts +2 -0
  42. package/dist/cjs/services/providers/openai/OpenAIChatService.d.ts.map +1 -1
  43. package/dist/cjs/services/providers/openai/OpenAIChatService.js +42 -4
  44. package/dist/cjs/services/providers/openai/OpenAIChatService.js.map +1 -1
  45. package/dist/cjs/utils/openaiCompatibleSse.d.ts.map +1 -1
  46. package/dist/cjs/utils/openaiCompatibleSse.js +32 -6
  47. package/dist/cjs/utils/openaiCompatibleSse.js.map +1 -1
  48. package/dist/esm/constants/deepseek.d.ts +11 -0
  49. package/dist/esm/constants/deepseek.d.ts.map +1 -0
  50. package/dist/esm/constants/deepseek.js +19 -0
  51. package/dist/esm/constants/deepseek.js.map +1 -0
  52. package/dist/esm/constants/index.d.ts +2 -0
  53. package/dist/esm/constants/index.d.ts.map +1 -1
  54. package/dist/esm/constants/index.js +2 -0
  55. package/dist/esm/constants/index.js.map +1 -1
  56. package/dist/esm/constants/mistral.d.ts +16 -0
  57. package/dist/esm/constants/mistral.d.ts.map +1 -0
  58. package/dist/esm/constants/mistral.js +39 -0
  59. package/dist/esm/constants/mistral.js.map +1 -0
  60. package/dist/esm/index.d.ts +5 -1
  61. package/dist/esm/index.d.ts.map +1 -1
  62. package/dist/esm/index.js +6 -0
  63. package/dist/esm/index.js.map +1 -1
  64. package/dist/esm/services/providers/ChatServiceProvider.d.ts +9 -1
  65. package/dist/esm/services/providers/ChatServiceProvider.d.ts.map +1 -1
  66. package/dist/esm/services/providers/deepseek/DeepSeekChatService.d.ts +7 -0
  67. package/dist/esm/services/providers/deepseek/DeepSeekChatService.d.ts.map +1 -0
  68. package/dist/esm/services/providers/deepseek/DeepSeekChatService.js +8 -0
  69. package/dist/esm/services/providers/deepseek/DeepSeekChatService.js.map +1 -0
  70. package/dist/esm/services/providers/deepseek/DeepSeekChatServiceProvider.d.ts +16 -0
  71. package/dist/esm/services/providers/deepseek/DeepSeekChatServiceProvider.d.ts.map +1 -0
  72. package/dist/esm/services/providers/deepseek/DeepSeekChatServiceProvider.js +53 -0
  73. package/dist/esm/services/providers/deepseek/DeepSeekChatServiceProvider.js.map +1 -0
  74. package/dist/esm/services/providers/index.d.ts +3 -1
  75. package/dist/esm/services/providers/index.d.ts.map +1 -1
  76. package/dist/esm/services/providers/index.js +4 -0
  77. package/dist/esm/services/providers/index.js.map +1 -1
  78. package/dist/esm/services/providers/mistral/MistralChatService.d.ts +8 -0
  79. package/dist/esm/services/providers/mistral/MistralChatService.d.ts.map +1 -0
  80. package/dist/esm/services/providers/mistral/MistralChatService.js +8 -0
  81. package/dist/esm/services/providers/mistral/MistralChatService.js.map +1 -0
  82. package/dist/esm/services/providers/mistral/MistralChatServiceProvider.d.ts +17 -0
  83. package/dist/esm/services/providers/mistral/MistralChatServiceProvider.d.ts.map +1 -0
  84. package/dist/esm/services/providers/mistral/MistralChatServiceProvider.js +68 -0
  85. package/dist/esm/services/providers/mistral/MistralChatServiceProvider.js.map +1 -0
  86. package/dist/esm/services/providers/openai/OpenAIChatService.d.ts +2 -0
  87. package/dist/esm/services/providers/openai/OpenAIChatService.d.ts.map +1 -1
  88. package/dist/esm/services/providers/openai/OpenAIChatService.js +43 -5
  89. package/dist/esm/services/providers/openai/OpenAIChatService.js.map +1 -1
  90. package/dist/esm/utils/openaiCompatibleSse.d.ts.map +1 -1
  91. package/dist/esm/utils/openaiCompatibleSse.js +32 -6
  92. package/dist/esm/utils/openaiCompatibleSse.js.map +1 -1
  93. package/dist/umd/aituber-onair-chat.js +1715 -1410
  94. package/dist/umd/aituber-onair-chat.min.js +10 -10
  95. 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
- if (delta?.content) {
828
- onPartial(delta.content);
829
- appendTextBlock(textBlocks, delta.content);
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 if (choice?.message?.content) {
871
- blocks.push({ type: "text", text: choice.message.content });
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/utils/mcpSchemaFetcher.ts
1675
- var MCPSchemaFetcher = class {
1676
- /**
1677
- * Fetch tool schemas from MCP server
1678
- * @param serverConfig MCP server configuration
1679
- * @returns Array of tool definitions
1680
- */
1681
- static async fetchToolSchemas(serverConfig) {
1682
- try {
1683
- const headers = {
1684
- "Content-Type": "application/json"
1685
- };
1686
- if (serverConfig.authorization_token) {
1687
- headers["Authorization"] = `Bearer ${serverConfig.authorization_token}`;
1688
- }
1689
- const response = await ChatServiceHttpClient.post(
1690
- `${serverConfig.url}/tools`,
1691
- {},
1692
- headers
1693
- );
1694
- const toolsData = await response.json();
1695
- if (Array.isArray(toolsData.tools)) {
1696
- return toolsData.tools.map((tool) => ({
1697
- name: `mcp_${serverConfig.name}_${tool.name}`,
1698
- description: tool.description || `Tool from ${serverConfig.name} MCP server`,
1699
- parameters: tool.inputSchema || {
1700
- type: "object",
1701
- properties: {},
1702
- required: []
1703
- }
1704
- }));
1705
- }
1706
- return [
1707
- {
1708
- name: `mcp_${serverConfig.name}_search`,
1709
- description: `Search using ${serverConfig.name} MCP server`,
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
- required: ["query"]
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
- required: ["query"]
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
- * Fetch all tool schemas from multiple MCP servers
1747
- * @param mcpServers Array of MCP server configurations
1748
- * @returns Array of all tool definitions
1749
- */
1750
- static async fetchAllToolSchemas(mcpServers) {
1751
- const allSchemas = [];
1752
- for (const server of mcpServers) {
1753
- try {
1754
- const schemas = await this.fetchToolSchemas(server);
1755
- allSchemas.push(...schemas);
1756
- } catch (error) {
1757
- console.error(`Failed to fetch schemas from ${server.name}:`, error);
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
- return allSchemas;
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/gemini/GeminiChatService.ts
1765
- var GeminiChatService = class {
1766
- /**
1767
- * Constructor
1768
- * @param apiKey Google API key
1769
- * @param model Name of the model to use
1770
- * @param visionModel Name of the vision model
1771
- * @param tools Array of tool definitions
1772
- * @param mcpServers Array of MCP server configurations
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 = MODEL_GEMINI_3_1_FLASH_LITE, visionModel = MODEL_GEMINI_3_1_FLASH_LITE, tools = [], mcpServers = [], responseLength) {
1775
- /** Provider name */
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
- if (!GEMINI_VISION_SUPPORTED_MODELS.includes(visionModel)) {
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,1383 @@ If it's in another language, summarize in that language.
1852
1993
  return this.visionModel;
1853
1994
  }
1854
1995
  /**
1855
- * Get configured MCP servers
1856
- * @returns Array of MCP server configurations
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
- getMCPServers() {
1859
- return this.mcpServers;
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
- * Add MCP server configuration
1863
- * @param serverConfig MCP server configuration
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
- addMCPServer(serverConfig) {
1866
- this.mcpServers.push(serverConfig);
1867
- this.mcpSchemasInitialized = false;
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
- * Remove MCP server by name
1871
- * @param serverName Name of the server to remove
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
- removeMCPServer(serverName) {
1874
- this.mcpServers = this.mcpServers.filter(
1875
- (server) => server.name !== serverName
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
- * Check if MCP servers are configured
1881
- * @returns True if MCP servers are configured
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
- hasMCPServers() {
1884
- return this.mcpServers.length > 0;
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
- * Initialize MCP tool schemas by fetching from servers
1888
- * @private
2098
+ * Parse response based on endpoint type
1889
2099
  */
1890
- async initializeMCPSchemas() {
1891
- if (this.mcpSchemasInitialized || this.mcpServers.length === 0) {
1892
- return;
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
- try {
1895
- const timeoutPromise = new Promise(
1896
- (_, reject) => setTimeout(() => reject(new Error("MCP schema fetch timeout")), 5e3)
1897
- );
1898
- const schemasPromise = MCPSchemaFetcher.fetchAllToolSchemas(
1899
- this.mcpServers
1900
- );
1901
- this.mcpToolSchemas = await Promise.race([
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
- * Process chat messages
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
- async processChat(messages, onPartialResponse, onCompleteResponse) {
1932
- try {
1933
- await processChatWithOptionalTools({
1934
- hasTools: this.tools.length > 0 || this.mcpServers.length > 0,
1935
- runWithoutTools: async () => {
1936
- const res = await this.callGemini(messages, this.model, true);
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
- for (const msg of messages) {
1994
- const role = this.mapRoleToGemini(msg.role);
1995
- if (msg.tool_calls) {
1996
- pushCurrent();
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
- if (msg.role === "tool") {
2014
- pushCurrent();
2015
- const funcName = msg.name ?? this.callIdMap.get(msg.tool_call_id) ?? "result";
2016
- gemini.push({
2017
- role: "user",
2018
- parts: [
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
- pushCurrent();
2036
- return gemini;
2037
- }
2038
- /* ────────────────────────────────── */
2039
- /* HTTP call */
2040
- /* ────────────────────────────────── */
2041
- async callGemini(messages, model, stream = false, maxTokens) {
2042
- const hasVision = messages.some(
2043
- (m) => Array.isArray(m.content) && m.content.some(
2044
- (b) => b?.type === "image_url" || b?.inlineData
2045
- )
2046
- );
2047
- const contents = hasVision ? await this.convertVisionMessagesToGeminiFormat(
2048
- messages
2049
- ) : this.convertMessagesToGeminiFormat(messages);
2050
- const body = {
2051
- contents,
2052
- generationConfig: {
2053
- maxOutputTokens: maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength)
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
- const allToolDeclarations = [];
2063
- if (this.tools.length > 0) {
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
- if (this.mcpServers.length > 0) {
2073
- try {
2074
- await this.initializeMCPSchemas();
2075
- allToolDeclarations.push(
2076
- ...this.mcpToolSchemas.map((t) => ({
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
- if (allToolDeclarations.length > 0) {
2087
- body.tools = [
2088
- {
2089
- functionDeclarations: allToolDeclarations
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 fetchOnce = async (ver, payload) => {
2095
- const fn = stream ? "streamGenerateContent" : "generateContent";
2096
- const alt = stream ? "?alt=sse" : "";
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
- * Convert AITuber OnAir vision messages to Gemini format
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
- async convertVisionMessagesToGeminiFormat(messages) {
2135
- const geminiMessages = [];
2136
- let currentRole = null;
2137
- let currentParts = [];
2138
- for (const msg of messages) {
2139
- const role = this.mapRoleToGemini(msg.role);
2140
- if (msg.tool_calls) {
2141
- for (const call of msg.tool_calls) {
2142
- geminiMessages.push({
2143
- role: "model",
2144
- parts: [
2145
- {
2146
- functionCall: {
2147
- name: call.function.name,
2148
- args: JSON.parse(call.function.arguments || "{}")
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
- currentParts.push({ text: msg.content });
2220
+ cleanMsg.content = msg.content;
2183
2221
  } else if (Array.isArray(msg.content)) {
2184
- for (const block of msg.content) {
2222
+ cleanMsg.content = msg.content.map((block) => {
2185
2223
  if (block.type === "text") {
2186
- currentParts.push({ text: block.text });
2224
+ return {
2225
+ type: "input_text",
2226
+ text: block.text
2227
+ };
2187
2228
  } else if (block.type === "image_url") {
2188
- try {
2189
- const imageResponse = await ChatServiceHttpClient.get(
2190
- block.image_url.url
2191
- );
2192
- const imageBlob = await imageResponse.blob();
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
- if (currentRole && currentParts.length > 0) {
2210
- geminiMessages.push({
2211
- role: currentRole,
2212
- parts: [...currentParts]
2213
- });
2214
- }
2215
- return geminiMessages;
2240
+ return cleanMsg;
2241
+ });
2216
2242
  }
2217
- /**
2218
- * Convert Blob to Base64 string
2219
- * @param blob Image blob
2220
- * @returns Promise with base64 encoded string
2221
- */
2222
- blobToBase64(blob) {
2223
- return new Promise((resolve, reject) => {
2224
- const reader = new FileReader();
2225
- reader.onloadend = () => resolve(reader.result);
2226
- reader.onerror = reject;
2227
- reader.readAsDataURL(blob);
2243
+ cleanMessagesForMistralChatCompletions(messages) {
2244
+ return messages.map((msg) => {
2245
+ if (!Array.isArray(msg.content)) {
2246
+ return msg;
2247
+ }
2248
+ return {
2249
+ ...msg,
2250
+ content: msg.content.map((block) => {
2251
+ if (block.type === "image_url" && typeof block.image_url === "object" && typeof block.image_url?.url === "string") {
2252
+ return {
2253
+ type: "image_url",
2254
+ image_url: block.image_url.url
2255
+ };
2256
+ }
2257
+ return block;
2258
+ })
2259
+ };
2228
2260
  });
2229
2261
  }
2230
2262
  /**
2231
- * Map AITuber OnAir roles to Gemini roles
2232
- * @param role AITuber OnAir role
2233
- * @returns Gemini role
2263
+ * Build tools definition based on the endpoint type
2234
2264
  */
2235
- mapRoleToGemini(role) {
2236
- switch (role) {
2237
- case "system":
2238
- return "model";
2239
- // Gemini uses 'model' for system messages
2240
- case "user":
2241
- return "user";
2242
- case "assistant":
2243
- return "model";
2244
- default:
2245
- return "user";
2265
+ buildToolsDefinition() {
2266
+ const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2267
+ const toolDefs = [];
2268
+ if (this.tools.length > 0) {
2269
+ toolDefs.push(
2270
+ ...buildOpenAICompatibleTools(
2271
+ this.tools,
2272
+ isResponsesAPI ? "responses" : "chat-completions"
2273
+ )
2274
+ );
2275
+ }
2276
+ if (this.mcpServers.length > 0 && isResponsesAPI) {
2277
+ toolDefs.push(...this.buildMCPToolsDefinition());
2246
2278
  }
2279
+ return toolDefs;
2247
2280
  }
2248
- /* ────────────────────────────────────────────────────────── */
2249
- /* Convert NDJSON stream to common format */
2250
- /* ────────────────────────────────────────────────────────── */
2251
- async parseStream(res, onPartial, model) {
2252
- const reader = res.body.getReader();
2253
- const dec = new TextDecoder();
2254
- const textBlocks = [];
2255
- const toolBlocks = [];
2256
- let buf = "";
2257
- const flush = (payload) => {
2258
- if (!payload || payload === "[DONE]") return;
2259
- let obj;
2260
- try {
2261
- obj = JSON.parse(payload);
2262
- } catch {
2263
- return;
2281
+ /**
2282
+ * Build MCP tools definition for Responses API
2283
+ */
2284
+ buildMCPToolsDefinition() {
2285
+ return this.mcpServers.map((server) => {
2286
+ const mcpDef = {
2287
+ type: "mcp",
2288
+ // Using 'mcp' as indicated by the error message
2289
+ server_label: server.name,
2290
+ // Use server_label as required by API
2291
+ server_url: server.url
2292
+ // Use server_url instead of url
2293
+ };
2294
+ if (server.require_approval) {
2295
+ mcpDef.require_approval = server.require_approval;
2264
2296
  }
2265
- for (const cand of obj.candidates ?? []) {
2266
- for (const part of cand.content?.parts ?? []) {
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
- }
2297
+ if (server.tool_configuration?.allowed_tools) {
2298
+ mcpDef.allowed_tools = server.tool_configuration.allowed_tools;
2287
2299
  }
2288
- };
2289
- while (true) {
2290
- const { done, value } = await reader.read();
2291
- if (done) break;
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);
2300
+ if (server.authorization_token) {
2301
+ mcpDef.headers = {
2302
+ Authorization: `Bearer ${server.authorization_token}`
2303
+ };
2305
2304
  }
2306
- }
2307
- if (buf) flush(buf);
2308
- const blocks = [...textBlocks, ...toolBlocks];
2309
- return {
2310
- blocks,
2311
- stop_reason: toolBlocks.some((b) => b.type === "tool_use") ? "tool_use" : "end"
2312
- };
2305
+ return mcpDef;
2306
+ });
2313
2307
  }
2314
- /* ────────────────────────────────────────────────────────── */
2315
- /* Convert JSON of non-stream (= generateContent) */
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
- };
2308
+ async handleStream(res, onPartial) {
2309
+ return parseOpenAICompatibleTextStream(res, onPartial);
2347
2310
  }
2348
- /* ────────────────────────────────────────────────────────── */
2349
- /* chatOnce (text) */
2350
- /* ────────────────────────────────────────────────────────── */
2351
- async chatOnce(messages, stream = true, onPartialResponse = () => {
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);
2311
+ async parseStream(res, onPartial) {
2312
+ return parseOpenAICompatibleToolStream(res, onPartial, {
2313
+ appendTextBlock: StreamTextAccumulator.addTextBlock
2314
+ });
2355
2315
  }
2356
- /* ────────────────────────────────────────────────────────── */
2357
- /* visionChatOnce (images) */
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);
2316
+ parseOneShot(data) {
2317
+ return parseOpenAICompatibleOneShot(data);
2368
2318
  }
2369
- /* ────────────────────────────────────────────────────────── */
2370
- /* UUID helper */
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
- });
2319
+ usesCompatibleChatCompletions() {
2320
+ return OPENAI_COMPATIBLE_CHAT_COMPLETIONS_PROVIDERS.has(this.provider);
2378
2321
  }
2379
2322
  };
2380
2323
 
2381
- // src/services/providers/gemini/GeminiChatServiceProvider.ts
2382
- var GeminiChatServiceProvider = class {
2383
- /**
2384
- * Create a chat service instance
2385
- * @param options Service options
2386
- * @returns GeminiChatService instance
2387
- */
2324
+ // src/services/providers/deepseek/DeepSeekChatService.ts
2325
+ var DeepSeekChatService = class extends OpenAIChatService {
2326
+ constructor(apiKey, model = MODEL_DEEPSEEK_V4_FLASH, visionModel = model, tools, endpoint = ENDPOINT_DEEPSEEK_CHAT_COMPLETIONS_API, responseLength) {
2327
+ super(
2328
+ apiKey,
2329
+ model,
2330
+ visionModel,
2331
+ tools,
2332
+ endpoint,
2333
+ [],
2334
+ responseLength,
2335
+ void 0,
2336
+ void 0,
2337
+ false,
2338
+ "deepseek",
2339
+ false
2340
+ );
2341
+ }
2342
+ };
2343
+
2344
+ // src/services/providers/deepseek/DeepSeekChatServiceProvider.ts
2345
+ var DeepSeekChatServiceProvider = class {
2388
2346
  createChatService(options) {
2389
- const visionModel = resolveVisionModel({
2390
- model: options.model,
2391
- visionModel: options.visionModel,
2392
- defaultModel: this.getDefaultModel(),
2393
- defaultVisionModel: this.getDefaultModel(),
2394
- supportsVisionForModel: (model) => this.supportsVisionForModel(model),
2395
- validate: "resolved"
2396
- });
2397
- return new GeminiChatService(
2347
+ this.validateRequiredOptions(options);
2348
+ const model = options.model || this.getDefaultModel();
2349
+ const tools = options.tools;
2350
+ return new DeepSeekChatService(
2398
2351
  options.apiKey,
2399
- options.model || this.getDefaultModel(),
2400
- visionModel,
2401
- options.tools || [],
2402
- options.mcpServers || [],
2352
+ model,
2353
+ options.visionModel ?? model,
2354
+ tools,
2355
+ this.resolveEndpoint(options),
2403
2356
  options.responseLength
2404
2357
  );
2405
2358
  }
2406
- /**
2407
- * Get the provider name
2408
- * @returns Provider name ('gemini')
2409
- */
2410
2359
  getProviderName() {
2411
- return "gemini";
2360
+ return "deepseek";
2412
2361
  }
2413
- /**
2414
- * Get the list of supported models
2415
- * @returns Array of supported model names
2416
- */
2417
2362
  getSupportedModels() {
2418
- return [...GEMINI_RECOMMENDED_MODELS];
2363
+ return [...DEEPSEEK_SUPPORTED_MODELS];
2419
2364
  }
2420
- /**
2421
- * Get the default model
2422
- * @returns Default model name
2423
- */
2424
2365
  getDefaultModel() {
2425
- return MODEL_GEMINI_3_1_FLASH_LITE;
2366
+ return MODEL_DEEPSEEK_V4_FLASH;
2426
2367
  }
2427
- /**
2428
- * Check if this provider supports vision (image processing)
2429
- * @returns Vision support status (true)
2430
- */
2431
2368
  supportsVision() {
2432
- return this.getVisionSupportLevel() !== "unsupported";
2369
+ return false;
2433
2370
  }
2434
2371
  getVisionSupportLevel() {
2435
- return "supported";
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
- };
2372
+ return "unsupported";
2491
2373
  }
2492
- async visionChatOnce(_messages, _stream = false, _onPartialResponse = () => {
2493
- }, _maxTokens) {
2494
- throw new Error("Gemini Nano does not support vision capabilities.");
2374
+ supportsVisionForModel(_model) {
2375
+ return false;
2495
2376
  }
2496
- /**
2497
- * Core logic: extract system prompt, manage session, call prompt().
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
- }
2377
+ getVisionSupportLevelForModel(_model) {
2378
+ return "unsupported";
2532
2379
  }
2533
- /**
2534
- * Create a new LanguageModel session with system prompt and context history.
2535
- * Context history (excluding the last user message) is embedded in the system prompt.
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;
2380
+ validateRequiredOptions(options) {
2381
+ if (!options.apiKey?.trim()) {
2382
+ throw new Error("deepseek provider requires apiKey.");
2543
2383
  }
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
2384
  }
2554
- buildSystemPrompt(systemPrompt) {
2555
- const promptParts = [];
2556
- if (systemPrompt) {
2557
- promptParts.push(systemPrompt);
2385
+ resolveEndpoint(options) {
2386
+ if (options.endpoint) {
2387
+ return this.normalizeUrl(options.endpoint);
2558
2388
  }
2559
- const lengthInstruction = this.getResponseLengthInstruction();
2560
- if (lengthInstruction) {
2561
- promptParts.push(lengthInstruction);
2389
+ if (options.baseUrl) {
2390
+ const baseUrl = this.normalizeUrl(options.baseUrl);
2391
+ if (baseUrl.endsWith("/chat/completions")) {
2392
+ return baseUrl;
2393
+ }
2394
+ return `${baseUrl}/chat/completions`;
2562
2395
  }
2563
- return promptParts.join("\n\n");
2396
+ return ENDPOINT_DEEPSEEK_CHAT_COMPLETIONS_API;
2564
2397
  }
2565
- getResponseLengthInstruction() {
2566
- if (!this._responseLength) {
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.`;
2398
+ normalizeUrl(value) {
2399
+ return value.trim().replace(/\/+$/, "");
2574
2400
  }
2575
2401
  };
2576
2402
 
2577
- // src/services/providers/geminiNano/GeminiNanoChatServiceProvider.ts
2578
- var GeminiNanoChatServiceProvider = class {
2579
- createChatService(options) {
2580
- return new GeminiNanoChatService({
2581
- expectedInputLanguages: options.expectedInputLanguages,
2582
- expectedOutputLanguages: options.expectedOutputLanguages,
2583
- responseLength: options.responseLength
2584
- });
2585
- }
2586
- getProviderName() {
2587
- return "gemini-nano";
2588
- }
2589
- getSupportedModels() {
2590
- return [MODEL_GEMINI_NANO];
2591
- }
2592
- getDefaultModel() {
2593
- return MODEL_GEMINI_NANO;
2594
- }
2595
- supportsVision() {
2596
- return false;
2403
+ // src/utils/mcpSchemaFetcher.ts
2404
+ var MCPSchemaFetcher = class {
2405
+ /**
2406
+ * Fetch tool schemas from MCP server
2407
+ * @param serverConfig MCP server configuration
2408
+ * @returns Array of tool definitions
2409
+ */
2410
+ static async fetchToolSchemas(serverConfig) {
2411
+ try {
2412
+ const headers = {
2413
+ "Content-Type": "application/json"
2414
+ };
2415
+ if (serverConfig.authorization_token) {
2416
+ headers["Authorization"] = `Bearer ${serverConfig.authorization_token}`;
2417
+ }
2418
+ const response = await ChatServiceHttpClient.post(
2419
+ `${serverConfig.url}/tools`,
2420
+ {},
2421
+ headers
2422
+ );
2423
+ const toolsData = await response.json();
2424
+ if (Array.isArray(toolsData.tools)) {
2425
+ return toolsData.tools.map((tool) => ({
2426
+ name: `mcp_${serverConfig.name}_${tool.name}`,
2427
+ description: tool.description || `Tool from ${serverConfig.name} MCP server`,
2428
+ parameters: tool.inputSchema || {
2429
+ type: "object",
2430
+ properties: {},
2431
+ required: []
2432
+ }
2433
+ }));
2434
+ }
2435
+ return [
2436
+ {
2437
+ name: `mcp_${serverConfig.name}_search`,
2438
+ description: `Search using ${serverConfig.name} MCP server`,
2439
+ parameters: {
2440
+ type: "object",
2441
+ properties: {
2442
+ query: {
2443
+ type: "string",
2444
+ description: "Search query"
2445
+ }
2446
+ },
2447
+ required: ["query"]
2448
+ }
2449
+ }
2450
+ ];
2451
+ } catch (error) {
2452
+ console.warn(
2453
+ `Failed to fetch MCP schemas from ${serverConfig.name}:`,
2454
+ error
2455
+ );
2456
+ return [
2457
+ {
2458
+ name: `mcp_${serverConfig.name}_search`,
2459
+ description: `Search using ${serverConfig.name} MCP server (schema fetch failed)`,
2460
+ parameters: {
2461
+ type: "object",
2462
+ properties: {
2463
+ query: {
2464
+ type: "string",
2465
+ description: "Search query"
2466
+ }
2467
+ },
2468
+ required: ["query"]
2469
+ }
2470
+ }
2471
+ ];
2472
+ }
2597
2473
  }
2598
- getVisionSupportLevel() {
2599
- return "unsupported";
2474
+ /**
2475
+ * Fetch all tool schemas from multiple MCP servers
2476
+ * @param mcpServers Array of MCP server configurations
2477
+ * @returns Array of all tool definitions
2478
+ */
2479
+ static async fetchAllToolSchemas(mcpServers) {
2480
+ const allSchemas = [];
2481
+ for (const server of mcpServers) {
2482
+ try {
2483
+ const schemas = await this.fetchToolSchemas(server);
2484
+ allSchemas.push(...schemas);
2485
+ } catch (error) {
2486
+ console.error(`Failed to fetch schemas from ${server.name}:`, error);
2487
+ }
2488
+ }
2489
+ return allSchemas;
2600
2490
  }
2601
2491
  };
2602
2492
 
2603
- // src/services/providers/kimi/KimiChatService.ts
2604
- var KimiChatService = class {
2493
+ // src/services/providers/gemini/GeminiChatService.ts
2494
+ var GeminiChatService = class {
2605
2495
  /**
2606
2496
  * Constructor
2607
- * @param apiKey Kimi API key
2497
+ * @param apiKey Google API key
2608
2498
  * @param model Name of the model to use
2609
2499
  * @param visionModel Name of the vision model
2500
+ * @param tools Array of tool definitions
2501
+ * @param mcpServers Array of MCP server configurations
2610
2502
  */
2611
- constructor(apiKey, model = MODEL_KIMI_K2_6, visionModel = MODEL_KIMI_K2_6, tools, endpoint = ENDPOINT_KIMI_CHAT_COMPLETIONS_API, responseLength, responseFormat, thinking) {
2503
+ constructor(apiKey, model = MODEL_GEMINI_3_1_FLASH_LITE, visionModel = MODEL_GEMINI_3_1_FLASH_LITE, tools = [], mcpServers = [], responseLength) {
2612
2504
  /** Provider name */
2613
- this.provider = "kimi";
2505
+ this.provider = "gemini";
2506
+ this.mcpToolSchemas = [];
2507
+ this.mcpSchemasInitialized = false;
2508
+ /** id(OpenAI) → name(Gemini) mapping */
2509
+ this.callIdMap = /* @__PURE__ */ new Map();
2614
2510
  this.apiKey = apiKey;
2615
2511
  this.model = model;
2616
- this.tools = tools || [];
2617
- this.endpoint = endpoint;
2618
2512
  this.responseLength = responseLength;
2619
- this.responseFormat = responseFormat;
2620
- this.thinking = thinking ?? { type: "enabled" };
2513
+ if (!GEMINI_VISION_SUPPORTED_MODELS.includes(visionModel)) {
2514
+ throw new Error(
2515
+ `Model ${visionModel} does not support vision capabilities.`
2516
+ );
2517
+ }
2621
2518
  this.visionModel = visionModel;
2519
+ this.tools = tools;
2520
+ this.mcpServers = mcpServers;
2521
+ }
2522
+ /* ────────────────────────────────── */
2523
+ /* Utilities */
2524
+ /* ────────────────────────────────── */
2525
+ safeJsonParse(str) {
2526
+ try {
2527
+ return JSON.parse(str);
2528
+ } catch {
2529
+ return str;
2530
+ }
2531
+ }
2532
+ normalizeToolResult(val) {
2533
+ if (val === null) return { content: null };
2534
+ if (typeof val === "object") return val;
2535
+ return { content: val };
2536
+ }
2537
+ isGemma4Model(model) {
2538
+ return /^gemma-4-/.test(model);
2539
+ }
2540
+ shouldExposeTextPart(part, model) {
2541
+ if (!part.text) return false;
2542
+ if (this.isGemma4Model(model) && part.thought === true) {
2543
+ return false;
2544
+ }
2545
+ return true;
2546
+ }
2547
+ /**
2548
+ * camelCase → snake_case conversion (v1beta)
2549
+ */
2550
+ adaptKeysForApi(obj) {
2551
+ const map = {
2552
+ toolConfig: "tool_config",
2553
+ functionCallingConfig: "function_calling_config",
2554
+ functionDeclarations: "function_declarations",
2555
+ functionCall: "function_call",
2556
+ functionResponse: "function_response"
2557
+ };
2558
+ if (Array.isArray(obj)) return obj.map((v) => this.adaptKeysForApi(v));
2559
+ if (obj && typeof obj === "object") {
2560
+ return Object.fromEntries(
2561
+ Object.entries(obj).map(([k, v]) => [
2562
+ map[k] ?? k,
2563
+ this.adaptKeysForApi(v)
2564
+ ])
2565
+ );
2566
+ }
2567
+ return obj;
2622
2568
  }
2623
2569
  /**
2624
2570
  * Get the current model name
2571
+ * @returns Model name
2625
2572
  */
2626
2573
  getModel() {
2627
2574
  return this.model;
2628
2575
  }
2629
2576
  /**
2630
2577
  * Get the current vision model name
2578
+ * @returns Vision model name
2631
2579
  */
2632
2580
  getVisionModel() {
2633
2581
  return this.visionModel;
2634
2582
  }
2635
2583
  /**
2636
- * Process chat messages
2584
+ * Get configured MCP servers
2585
+ * @returns Array of MCP server configurations
2637
2586
  */
2638
- async processChat(messages, onPartialResponse, onCompleteResponse) {
2639
- await processChatWithOptionalTools({
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
- });
2587
+ getMCPServers() {
2588
+ return this.mcpServers;
2649
2589
  }
2650
2590
  /**
2651
- * Process chat messages with images
2591
+ * Add MCP server configuration
2592
+ * @param serverConfig MCP server configuration
2652
2593
  */
2653
- async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
2654
- if (!isKimiVisionModel(this.visionModel)) {
2655
- throw new Error(
2656
- `Model ${this.visionModel} does not support vision capabilities.`
2594
+ addMCPServer(serverConfig) {
2595
+ this.mcpServers.push(serverConfig);
2596
+ this.mcpSchemasInitialized = false;
2597
+ }
2598
+ /**
2599
+ * Remove MCP server by name
2600
+ * @param serverName Name of the server to remove
2601
+ */
2602
+ removeMCPServer(serverName) {
2603
+ this.mcpServers = this.mcpServers.filter(
2604
+ (server) => server.name !== serverName
2605
+ );
2606
+ this.mcpSchemasInitialized = false;
2607
+ }
2608
+ /**
2609
+ * Check if MCP servers are configured
2610
+ * @returns True if MCP servers are configured
2611
+ */
2612
+ hasMCPServers() {
2613
+ return this.mcpServers.length > 0;
2614
+ }
2615
+ /**
2616
+ * Initialize MCP tool schemas by fetching from servers
2617
+ * @private
2618
+ */
2619
+ async initializeMCPSchemas() {
2620
+ if (this.mcpSchemasInitialized || this.mcpServers.length === 0) {
2621
+ return;
2622
+ }
2623
+ try {
2624
+ const timeoutPromise = new Promise(
2625
+ (_, reject) => setTimeout(() => reject(new Error("MCP schema fetch timeout")), 5e3)
2626
+ );
2627
+ const schemasPromise = MCPSchemaFetcher.fetchAllToolSchemas(
2628
+ this.mcpServers
2657
2629
  );
2630
+ this.mcpToolSchemas = await Promise.race([
2631
+ schemasPromise,
2632
+ timeoutPromise
2633
+ ]);
2634
+ this.mcpSchemasInitialized = true;
2635
+ } catch (error) {
2636
+ console.warn("Failed to initialize MCP schemas, using fallback:", error);
2637
+ this.mcpToolSchemas = this.mcpServers.map((server) => ({
2638
+ name: `mcp_${server.name}_search`,
2639
+ description: `Search using ${server.name} MCP server (fallback)`,
2640
+ parameters: {
2641
+ type: "object",
2642
+ properties: {
2643
+ query: {
2644
+ type: "string",
2645
+ description: "Search query"
2646
+ }
2647
+ },
2648
+ required: ["query"]
2649
+ }
2650
+ }));
2651
+ this.mcpSchemasInitialized = true;
2652
+ }
2653
+ }
2654
+ /**
2655
+ * Process chat messages
2656
+ * @param messages Array of messages to send
2657
+ * @param onPartialResponse Callback to receive each part of streaming response
2658
+ * @param onCompleteResponse Callback to execute when response is complete
2659
+ */
2660
+ async processChat(messages, onPartialResponse, onCompleteResponse) {
2661
+ try {
2662
+ await processChatWithOptionalTools({
2663
+ hasTools: this.tools.length > 0 || this.mcpServers.length > 0,
2664
+ runWithoutTools: async () => {
2665
+ const res = await this.callGemini(messages, this.model, true);
2666
+ const { blocks } = await this.parseStream(
2667
+ res,
2668
+ onPartialResponse,
2669
+ this.model
2670
+ );
2671
+ return StreamTextAccumulator.getFullText(blocks);
2672
+ },
2673
+ runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
2674
+ onCompleteResponse,
2675
+ toolErrorMessage: "Received functionCall. Use chatOnce() loop when tools are enabled."
2676
+ });
2677
+ } catch (err) {
2678
+ console.error("Error in processChat:", err);
2679
+ throw err;
2680
+ }
2681
+ }
2682
+ async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
2683
+ try {
2684
+ await processChatWithOptionalTools({
2685
+ hasTools: this.tools.length > 0 || this.mcpServers.length > 0,
2686
+ runWithoutTools: async () => {
2687
+ const res = await this.callGemini(messages, this.visionModel, true);
2688
+ const { blocks } = await this.parseStream(
2689
+ res,
2690
+ onPartialResponse,
2691
+ this.visionModel
2692
+ );
2693
+ return StreamTextAccumulator.getFullText(blocks);
2694
+ },
2695
+ runWithTools: () => this.visionChatOnce(messages),
2696
+ onToolBlocks: (blocks) => {
2697
+ blocks.filter(
2698
+ (b) => b.type === "text"
2699
+ ).forEach((b) => onPartialResponse(b.text));
2700
+ },
2701
+ onCompleteResponse,
2702
+ toolErrorMessage: "Received functionCall. Use visionChatOnce() loop when tools are enabled."
2703
+ });
2704
+ } catch (err) {
2705
+ console.error("Error in processVisionChat:", err);
2706
+ throw err;
2707
+ }
2708
+ }
2709
+ /* ────────────────────────────────── */
2710
+ /* OpenAI → Gemini conversion */
2711
+ /* ────────────────────────────────── */
2712
+ convertMessagesToGeminiFormat(messages) {
2713
+ const gemini = [];
2714
+ let currentRole = null;
2715
+ let currentParts = [];
2716
+ const pushCurrent = () => {
2717
+ if (currentRole && currentParts.length) {
2718
+ gemini.push({ role: currentRole, parts: [...currentParts] });
2719
+ currentParts = [];
2720
+ }
2721
+ };
2722
+ for (const msg of messages) {
2723
+ const role = this.mapRoleToGemini(msg.role);
2724
+ if (msg.tool_calls) {
2725
+ pushCurrent();
2726
+ for (const call of msg.tool_calls) {
2727
+ this.callIdMap.set(call.id, call.function.name);
2728
+ gemini.push({
2729
+ role: "model",
2730
+ parts: [
2731
+ {
2732
+ functionCall: {
2733
+ name: call.function.name,
2734
+ args: JSON.parse(call.function.arguments || "{}")
2735
+ }
2736
+ }
2737
+ ]
2738
+ });
2739
+ }
2740
+ continue;
2741
+ }
2742
+ if (msg.role === "tool") {
2743
+ pushCurrent();
2744
+ const funcName = msg.name ?? this.callIdMap.get(msg.tool_call_id) ?? "result";
2745
+ gemini.push({
2746
+ role: "user",
2747
+ parts: [
2748
+ {
2749
+ functionResponse: {
2750
+ name: funcName,
2751
+ response: this.normalizeToolResult(
2752
+ this.safeJsonParse(msg.content)
2753
+ )
2754
+ }
2755
+ }
2756
+ ]
2757
+ });
2758
+ continue;
2759
+ }
2760
+ if (role !== currentRole) pushCurrent();
2761
+ currentRole = role;
2762
+ currentParts.push({ text: msg.content });
2763
+ }
2764
+ pushCurrent();
2765
+ return gemini;
2766
+ }
2767
+ /* ────────────────────────────────── */
2768
+ /* HTTP call */
2769
+ /* ────────────────────────────────── */
2770
+ async callGemini(messages, model, stream = false, maxTokens) {
2771
+ const hasVision = messages.some(
2772
+ (m) => Array.isArray(m.content) && m.content.some(
2773
+ (b) => b?.type === "image_url" || b?.inlineData
2774
+ )
2775
+ );
2776
+ const contents = hasVision ? await this.convertVisionMessagesToGeminiFormat(
2777
+ messages
2778
+ ) : this.convertMessagesToGeminiFormat(messages);
2779
+ const body = {
2780
+ contents,
2781
+ generationConfig: {
2782
+ maxOutputTokens: maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength)
2783
+ }
2784
+ };
2785
+ if (this.isGemma4Model(model)) {
2786
+ body.generationConfig.thinkingConfig = {
2787
+ includeThoughts: false,
2788
+ thinkingLevel: "minimal"
2789
+ };
2790
+ }
2791
+ const allToolDeclarations = [];
2792
+ if (this.tools.length > 0) {
2793
+ allToolDeclarations.push(
2794
+ ...this.tools.map((t) => ({
2795
+ name: t.name,
2796
+ description: t.description,
2797
+ parameters: t.parameters
2798
+ }))
2799
+ );
2800
+ }
2801
+ if (this.mcpServers.length > 0) {
2802
+ try {
2803
+ await this.initializeMCPSchemas();
2804
+ allToolDeclarations.push(
2805
+ ...this.mcpToolSchemas.map((t) => ({
2806
+ name: t.name,
2807
+ description: t.description,
2808
+ parameters: t.parameters
2809
+ }))
2810
+ );
2811
+ } catch (error) {
2812
+ console.warn("MCP initialization failed, skipping MCP tools:", error);
2813
+ }
2814
+ }
2815
+ if (allToolDeclarations.length > 0) {
2816
+ body.tools = [
2817
+ {
2818
+ functionDeclarations: allToolDeclarations
2819
+ }
2820
+ ];
2821
+ body.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
2822
+ }
2823
+ const fetchOnce = async (ver, payload) => {
2824
+ const fn = stream ? "streamGenerateContent" : "generateContent";
2825
+ const alt = stream ? "?alt=sse" : "";
2826
+ const url = `${ENDPOINT_GEMINI_API}/${ver}/models/${model}:${fn}${alt}${alt ? "&" : "?"}key=${this.apiKey}`;
2827
+ return ChatServiceHttpClient.post(url, payload);
2828
+ };
2829
+ const isLite = /flash[-_]lite/.test(model);
2830
+ const isGemma4 = this.isGemma4Model(model);
2831
+ const isGemini25 = /gemini-2\.5/.test(model);
2832
+ const isGemini3Preview = /^gemini-3(?:\.[0-9]+)?-.*preview/.test(model);
2833
+ const requiresV1beta = isLite || isGemma4 || isGemini25 || isGemini3Preview;
2834
+ const firstVer = requiresV1beta ? "v1beta" : "v1";
2835
+ const tryApi = async () => {
2836
+ try {
2837
+ const payload = firstVer === "v1" ? body : this.adaptKeysForApi(body);
2838
+ return await fetchOnce(firstVer, payload);
2839
+ } catch (e) {
2840
+ const looksLikeVersionMismatch = /Unknown name|Cannot find field|404/.test(e?.message || "") || e?.status === 404;
2841
+ if (!requiresV1beta && looksLikeVersionMismatch) {
2842
+ return await fetchOnce("v1beta", this.adaptKeysForApi(body));
2843
+ }
2844
+ throw e;
2845
+ }
2846
+ };
2847
+ try {
2848
+ const res = await tryApi();
2849
+ return res;
2850
+ } catch (error) {
2851
+ if (error.body) {
2852
+ console.error("Gemini API Error Details:", error.body);
2853
+ console.error("Request Body:", JSON.stringify(body, null, 2));
2854
+ }
2855
+ throw error;
2856
+ }
2857
+ }
2858
+ /**
2859
+ * Convert AITuber OnAir vision messages to Gemini format
2860
+ * @param messages Array of vision messages
2861
+ * @returns Gemini formatted vision messages
2862
+ */
2863
+ async convertVisionMessagesToGeminiFormat(messages) {
2864
+ const geminiMessages = [];
2865
+ let currentRole = null;
2866
+ let currentParts = [];
2867
+ for (const msg of messages) {
2868
+ const role = this.mapRoleToGemini(msg.role);
2869
+ if (msg.tool_calls) {
2870
+ for (const call of msg.tool_calls) {
2871
+ geminiMessages.push({
2872
+ role: "model",
2873
+ parts: [
2874
+ {
2875
+ functionCall: {
2876
+ name: call.function.name,
2877
+ args: JSON.parse(call.function.arguments || "{}")
2878
+ }
2879
+ }
2880
+ ]
2881
+ });
2882
+ }
2883
+ continue;
2884
+ }
2885
+ if (msg.role === "tool") {
2886
+ const funcName = msg.name ?? this.callIdMap.get(msg.tool_call_id) ?? "result";
2887
+ geminiMessages.push({
2888
+ role: "user",
2889
+ parts: [
2890
+ {
2891
+ functionResponse: {
2892
+ name: funcName,
2893
+ response: this.normalizeToolResult(
2894
+ this.safeJsonParse(msg.content)
2895
+ )
2896
+ }
2897
+ }
2898
+ ]
2899
+ });
2900
+ continue;
2901
+ }
2902
+ if (role !== currentRole && currentParts.length > 0) {
2903
+ geminiMessages.push({
2904
+ role: currentRole,
2905
+ parts: [...currentParts]
2906
+ });
2907
+ currentParts = [];
2908
+ }
2909
+ currentRole = role;
2910
+ if (typeof msg.content === "string") {
2911
+ currentParts.push({ text: msg.content });
2912
+ } else if (Array.isArray(msg.content)) {
2913
+ for (const block of msg.content) {
2914
+ if (block.type === "text") {
2915
+ currentParts.push({ text: block.text });
2916
+ } else if (block.type === "image_url") {
2917
+ try {
2918
+ const imageResponse = await ChatServiceHttpClient.get(
2919
+ block.image_url.url
2920
+ );
2921
+ const imageBlob = await imageResponse.blob();
2922
+ const base64Data = await this.blobToBase64(imageBlob);
2923
+ currentParts.push({
2924
+ inlineData: {
2925
+ mimeType: imageBlob.type || "image/jpeg",
2926
+ data: base64Data.split(",")[1]
2927
+ // Remove the "data:image/jpeg;base64," prefix
2928
+ }
2929
+ });
2930
+ } catch (error) {
2931
+ console.error("Error processing image:", error);
2932
+ throw new Error(`Failed to process image: ${error.message}`);
2933
+ }
2934
+ }
2935
+ }
2936
+ }
2937
+ }
2938
+ if (currentRole && currentParts.length > 0) {
2939
+ geminiMessages.push({
2940
+ role: currentRole,
2941
+ parts: [...currentParts]
2942
+ });
2943
+ }
2944
+ return geminiMessages;
2945
+ }
2946
+ /**
2947
+ * Convert Blob to Base64 string
2948
+ * @param blob Image blob
2949
+ * @returns Promise with base64 encoded string
2950
+ */
2951
+ blobToBase64(blob) {
2952
+ return new Promise((resolve, reject) => {
2953
+ const reader = new FileReader();
2954
+ reader.onloadend = () => resolve(reader.result);
2955
+ reader.onerror = reject;
2956
+ reader.readAsDataURL(blob);
2957
+ });
2958
+ }
2959
+ /**
2960
+ * Map AITuber OnAir roles to Gemini roles
2961
+ * @param role AITuber OnAir role
2962
+ * @returns Gemini role
2963
+ */
2964
+ mapRoleToGemini(role) {
2965
+ switch (role) {
2966
+ case "system":
2967
+ return "model";
2968
+ // Gemini uses 'model' for system messages
2969
+ case "user":
2970
+ return "user";
2971
+ case "assistant":
2972
+ return "model";
2973
+ default:
2974
+ return "user";
2975
+ }
2976
+ }
2977
+ /* ────────────────────────────────────────────────────────── */
2978
+ /* Convert NDJSON stream to common format */
2979
+ /* ────────────────────────────────────────────────────────── */
2980
+ async parseStream(res, onPartial, model) {
2981
+ const reader = res.body.getReader();
2982
+ const dec = new TextDecoder();
2983
+ const textBlocks = [];
2984
+ const toolBlocks = [];
2985
+ let buf = "";
2986
+ const flush = (payload) => {
2987
+ if (!payload || payload === "[DONE]") return;
2988
+ let obj;
2989
+ try {
2990
+ obj = JSON.parse(payload);
2991
+ } catch {
2992
+ return;
2993
+ }
2994
+ for (const cand of obj.candidates ?? []) {
2995
+ for (const part of cand.content?.parts ?? []) {
2996
+ if (this.shouldExposeTextPart(part, model)) {
2997
+ onPartial(part.text);
2998
+ StreamTextAccumulator.addTextBlock(textBlocks, part.text);
2999
+ }
3000
+ if (part.functionCall) {
3001
+ toolBlocks.push({
3002
+ type: "tool_use",
3003
+ id: this.genUUID(),
3004
+ name: part.functionCall.name,
3005
+ input: part.functionCall.args ?? {}
3006
+ });
3007
+ }
3008
+ if (part.functionResponse) {
3009
+ toolBlocks.push({
3010
+ type: "tool_result",
3011
+ tool_use_id: part.functionResponse.name,
3012
+ content: JSON.stringify(part.functionResponse.response)
3013
+ });
3014
+ }
3015
+ }
3016
+ }
3017
+ };
3018
+ while (true) {
3019
+ const { done, value } = await reader.read();
3020
+ if (done) break;
3021
+ buf += dec.decode(value, { stream: true });
3022
+ let nl;
3023
+ while ((nl = buf.indexOf("\n")) !== -1) {
3024
+ let line = buf.slice(0, nl);
3025
+ buf = buf.slice(nl + 1);
3026
+ if (line.endsWith("\r")) line = line.slice(0, -1);
3027
+ if (!line.trim()) {
3028
+ flush("");
3029
+ continue;
3030
+ }
3031
+ if (line.startsWith("data:")) line = line.slice(5).trim();
3032
+ if (!line) continue;
3033
+ flush(line);
3034
+ }
3035
+ }
3036
+ if (buf) flush(buf);
3037
+ const blocks = [...textBlocks, ...toolBlocks];
3038
+ return {
3039
+ blocks,
3040
+ stop_reason: toolBlocks.some((b) => b.type === "tool_use") ? "tool_use" : "end"
3041
+ };
3042
+ }
3043
+ /* ────────────────────────────────────────────────────────── */
3044
+ /* Convert JSON of non-stream (= generateContent) */
3045
+ /* ────────────────────────────────────────────────────────── */
3046
+ parseOneShot(data, model) {
3047
+ const textBlocks = [];
3048
+ const toolBlocks = [];
3049
+ for (const cand of data.candidates ?? []) {
3050
+ for (const part of cand.content?.parts ?? []) {
3051
+ if (this.shouldExposeTextPart(part, model)) {
3052
+ textBlocks.push({ type: "text", text: part.text });
3053
+ }
3054
+ if (part.functionCall) {
3055
+ toolBlocks.push({
3056
+ type: "tool_use",
3057
+ id: this.genUUID(),
3058
+ name: part.functionCall.name,
3059
+ input: part.functionCall.args ?? {}
3060
+ });
3061
+ }
3062
+ if (part.functionResponse) {
3063
+ toolBlocks.push({
3064
+ type: "tool_result",
3065
+ tool_use_id: part.functionResponse.name,
3066
+ content: JSON.stringify(part.functionResponse.response)
3067
+ });
3068
+ }
3069
+ }
2658
3070
  }
2659
- await processChatWithOptionalTools({
2660
- hasTools: this.tools.length > 0,
2661
- runWithoutTools: async () => {
2662
- const res = await this.callKimi(messages, this.visionModel, true);
2663
- return this.handleStream(res, onPartialResponse);
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
- });
3071
+ const blocks = [...textBlocks, ...toolBlocks];
3072
+ return {
3073
+ blocks,
3074
+ stop_reason: toolBlocks.some((b) => b.type === "tool_use") ? "tool_use" : "end"
3075
+ };
2669
3076
  }
2670
- /**
2671
- * Process chat messages with tools (text only)
2672
- */
3077
+ /* ────────────────────────────────────────────────────────── */
3078
+ /* chatOnce (text) */
3079
+ /* ────────────────────────────────────────────────────────── */
2673
3080
  async chatOnce(messages, stream = true, onPartialResponse = () => {
2674
3081
  }, maxTokens) {
2675
- const res = await this.callKimi(messages, this.model, stream, maxTokens);
2676
- return this.parseResponse(res, stream, onPartialResponse);
3082
+ const res = await this.callGemini(messages, this.model, stream, maxTokens);
3083
+ return stream ? this.parseStream(res, onPartialResponse, this.model) : this.parseOneShot(await res.json(), this.model);
2677
3084
  }
2678
- /**
2679
- * Process vision chat messages with tools
2680
- */
3085
+ /* ────────────────────────────────────────────────────────── */
3086
+ /* visionChatOnce (images) */
3087
+ /* ────────────────────────────────────────────────────────── */
2681
3088
  async visionChatOnce(messages, stream = false, onPartialResponse = () => {
2682
3089
  }, maxTokens) {
2683
- if (!isKimiVisionModel(this.visionModel)) {
2684
- throw new Error(
2685
- `Model ${this.visionModel} does not support vision capabilities.`
2686
- );
2687
- }
2688
- const res = await this.callKimi(
3090
+ const res = await this.callGemini(
2689
3091
  messages,
2690
3092
  this.visionModel,
2691
3093
  stream,
2692
3094
  maxTokens
2693
3095
  );
2694
- return this.parseResponse(res, stream, onPartialResponse);
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
- });
3096
+ return stream ? this.parseStream(res, onPartialResponse, this.visionModel) : this.parseOneShot(await res.json(), this.visionModel);
2752
3097
  }
2753
- /**
2754
- * Parse streaming response with tool support
2755
- */
2756
- async parseStream(res, onPartial) {
2757
- return parseOpenAICompatibleToolStream(res, onPartial, {
2758
- onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3098
+ /* ────────────────────────────────────────────────────────── */
3099
+ /* UUID helper */
3100
+ /* ────────────────────────────────────────────────────────── */
3101
+ genUUID() {
3102
+ return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
3103
+ const r = Math.random() * 16 | 0;
3104
+ const v = c === "x" ? r : r & 3 | 8;
3105
+ return v.toString(16);
2759
3106
  });
2760
3107
  }
2761
- /**
2762
- * Parse non-streaming response
2763
- */
2764
- parseOneShot(data) {
2765
- return parseOpenAICompatibleOneShot(data);
2766
- }
2767
3108
  };
2768
3109
 
2769
- // src/services/providers/kimi/KimiChatServiceProvider.ts
2770
- var KimiChatServiceProvider = class {
3110
+ // src/services/providers/gemini/GeminiChatServiceProvider.ts
3111
+ var GeminiChatServiceProvider = class {
2771
3112
  /**
2772
3113
  * Create a chat service instance
3114
+ * @param options Service options
3115
+ * @returns GeminiChatService instance
2773
3116
  */
2774
3117
  createChatService(options) {
2775
- const endpoint = this.resolveEndpoint(options);
2776
- const model = options.model || this.getDefaultModel();
2777
3118
  const visionModel = resolveVisionModel({
2778
- model,
3119
+ model: options.model,
2779
3120
  visionModel: options.visionModel,
2780
3121
  defaultModel: this.getDefaultModel(),
2781
- defaultVisionModel: this.getDefaultVisionModel(),
2782
- supportsVisionForModel: (visionModel2) => this.supportsVisionForModel(visionModel2),
2783
- validate: "explicit"
3122
+ defaultVisionModel: this.getDefaultModel(),
3123
+ supportsVisionForModel: (model) => this.supportsVisionForModel(model),
3124
+ validate: "resolved"
2784
3125
  });
2785
- const tools = options.tools;
2786
- const defaultThinking = options.thinking ?? { type: "enabled" };
2787
- const thinking = tools && tools.length > 0 ? { type: "disabled" } : defaultThinking;
2788
- return new KimiChatService(
3126
+ return new GeminiChatService(
2789
3127
  options.apiKey,
2790
- model,
3128
+ options.model || this.getDefaultModel(),
2791
3129
  visionModel,
2792
- tools,
2793
- endpoint,
2794
- options.responseLength,
2795
- options.responseFormat,
2796
- thinking
3130
+ options.tools || [],
3131
+ options.mcpServers || [],
3132
+ options.responseLength
2797
3133
  );
2798
3134
  }
2799
3135
  /**
2800
3136
  * Get the provider name
3137
+ * @returns Provider name ('gemini')
2801
3138
  */
2802
3139
  getProviderName() {
2803
- return "kimi";
3140
+ return "gemini";
2804
3141
  }
2805
3142
  /**
2806
3143
  * Get the list of supported models
3144
+ * @returns Array of supported model names
2807
3145
  */
2808
3146
  getSupportedModels() {
2809
- return [MODEL_KIMI_K2_6, MODEL_KIMI_K2_5];
3147
+ return [...GEMINI_RECOMMENDED_MODELS];
2810
3148
  }
2811
3149
  /**
2812
3150
  * Get the default model
3151
+ * @returns Default model name
2813
3152
  */
2814
3153
  getDefaultModel() {
2815
- return MODEL_KIMI_K2_6;
3154
+ return MODEL_GEMINI_3_1_FLASH_LITE;
2816
3155
  }
2817
3156
  /**
2818
- * Get the default vision model
3157
+ * Check if this provider supports vision (image processing)
3158
+ * @returns Vision support status (true)
2819
3159
  */
2820
- getDefaultVisionModel() {
2821
- return MODEL_KIMI_K2_6;
3160
+ supportsVision() {
3161
+ return this.getVisionSupportLevel() !== "unsupported";
3162
+ }
3163
+ getVisionSupportLevel() {
3164
+ return "supported";
3165
+ }
3166
+ /**
3167
+ * Check if a specific model supports vision capabilities
3168
+ * @param model The model name to check
3169
+ * @returns True if the model supports vision, false otherwise
3170
+ */
3171
+ supportsVisionForModel(model) {
3172
+ return GEMINI_VISION_SUPPORTED_MODELS.includes(model);
3173
+ }
3174
+ getVisionSupportLevelForModel(model) {
3175
+ return this.supportsVisionForModel(model) ? "supported" : "unsupported";
3176
+ }
3177
+ };
3178
+
3179
+ // src/services/providers/geminiNano/GeminiNanoChatService.ts
3180
+ function getLanguageModelAPI() {
3181
+ if (typeof globalThis !== "undefined" && "LanguageModel" in globalThis) {
3182
+ return globalThis.LanguageModel;
3183
+ }
3184
+ return void 0;
3185
+ }
3186
+ var GeminiNanoChatService = class {
3187
+ constructor(options = {}) {
3188
+ this.provider = "gemini-nano";
3189
+ this.expectedInputLanguages = options.expectedInputLanguages ?? ["ja"];
3190
+ this.expectedOutputLanguages = options.expectedOutputLanguages ?? ["ja"];
3191
+ this._responseLength = options.responseLength;
3192
+ }
3193
+ getModel() {
3194
+ return MODEL_GEMINI_NANO;
3195
+ }
3196
+ getVisionModel() {
3197
+ return MODEL_GEMINI_NANO;
3198
+ }
3199
+ /**
3200
+ * Process chat messages using Gemini Nano.
3201
+ * Non-streaming: calls onPartialResponse once with the full response,
3202
+ * then calls onCompleteResponse.
3203
+ */
3204
+ async processChat(messages, onPartialResponse, onCompleteResponse) {
3205
+ const response = await this.generateResponse(messages);
3206
+ onPartialResponse(response);
3207
+ await onCompleteResponse(response);
3208
+ }
3209
+ async processVisionChat(_messages, _onPartialResponse, _onCompleteResponse) {
3210
+ throw new Error("Gemini Nano does not support vision capabilities.");
3211
+ }
3212
+ async chatOnce(messages, _stream = false, onPartialResponse = () => {
3213
+ }, _maxTokens) {
3214
+ const response = await this.generateResponse(messages);
3215
+ onPartialResponse(response);
3216
+ return {
3217
+ blocks: [{ type: "text", text: response }],
3218
+ stop_reason: "end"
3219
+ };
3220
+ }
3221
+ async visionChatOnce(_messages, _stream = false, _onPartialResponse = () => {
3222
+ }, _maxTokens) {
3223
+ throw new Error("Gemini Nano does not support vision capabilities.");
2822
3224
  }
2823
3225
  /**
2824
- * Check if this provider supports vision
3226
+ * Core logic: extract system prompt, manage session, call prompt().
2825
3227
  */
2826
- supportsVision() {
2827
- return this.getVisionSupportLevel() !== "unsupported";
2828
- }
2829
- getVisionSupportLevel() {
2830
- return "supported";
3228
+ async generateResponse(messages) {
3229
+ const api = getLanguageModelAPI();
3230
+ if (!api) {
3231
+ throw new Error(
3232
+ "Gemini Nano is not available in this environment. Chrome 138+ with Prompt API enabled is required."
3233
+ );
3234
+ }
3235
+ const availability = await api.availability();
3236
+ if (availability !== "available" && availability !== "downloadable") {
3237
+ throw new Error(
3238
+ `Gemini Nano Prompt API is not ready in this environment. LanguageModel.availability() returned "${availability}". Expected "available" or "downloadable".`
3239
+ );
3240
+ }
3241
+ const systemMessages = messages.filter((m) => m.role === "system");
3242
+ const systemPrompt = systemMessages.map((m) => m.content).join("\n");
3243
+ const conversationMessages = messages.filter((m) => m.role !== "system").slice(-GEMINI_NANO_MAX_CONTEXT_MESSAGES);
3244
+ const lastUserMessage = [...conversationMessages].reverse().find((m) => m.role === "user");
3245
+ if (!lastUserMessage) {
3246
+ throw new Error("No user message found in the provided messages.");
3247
+ }
3248
+ const session = await this.createSession(
3249
+ api,
3250
+ systemPrompt,
3251
+ conversationMessages
3252
+ );
3253
+ try {
3254
+ return await session.prompt(lastUserMessage.content);
3255
+ } finally {
3256
+ try {
3257
+ session.destroy();
3258
+ } catch {
3259
+ }
3260
+ }
2831
3261
  }
2832
3262
  /**
2833
- * Check if a specific model supports vision capabilities
3263
+ * Create a new LanguageModel session with system prompt and context history.
3264
+ * Context history (excluding the last user message) is embedded in the system prompt.
2834
3265
  */
2835
- supportsVisionForModel(model) {
2836
- return isKimiVisionModel(model);
2837
- }
2838
- getVisionSupportLevelForModel(model) {
2839
- return this.supportsVisionForModel(model) ? "supported" : "unsupported";
3266
+ async createSession(api, systemPrompt, contextHistory) {
3267
+ let prompt = this.buildSystemPrompt(systemPrompt);
3268
+ const historyMessages = contextHistory.slice(0, -1);
3269
+ if (historyMessages.length > 0) {
3270
+ const history = historyMessages.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n");
3271
+ prompt += "\n\nThe following is the prior conversation history. Use it as context for your response:\n" + history;
3272
+ }
3273
+ return api.create({
3274
+ systemPrompt: prompt,
3275
+ expectedInputs: [
3276
+ { type: "text", languages: this.expectedInputLanguages }
3277
+ ],
3278
+ expectedOutputs: [
3279
+ { type: "text", languages: this.expectedOutputLanguages }
3280
+ ]
3281
+ });
2840
3282
  }
2841
- resolveEndpoint(options) {
2842
- if (options.endpoint) {
2843
- return this.normalizeEndpoint(options.endpoint);
3283
+ buildSystemPrompt(systemPrompt) {
3284
+ const promptParts = [];
3285
+ if (systemPrompt) {
3286
+ promptParts.push(systemPrompt);
2844
3287
  }
2845
- if (options.baseUrl) {
2846
- const baseUrl = this.normalizeEndpoint(options.baseUrl);
2847
- if (baseUrl.endsWith("/chat/completions")) {
2848
- return baseUrl;
2849
- }
2850
- return `${baseUrl}/chat/completions`;
3288
+ const lengthInstruction = this.getResponseLengthInstruction();
3289
+ if (lengthInstruction) {
3290
+ promptParts.push(lengthInstruction);
2851
3291
  }
2852
- return ENDPOINT_KIMI_CHAT_COMPLETIONS_API;
3292
+ return promptParts.join("\n\n");
2853
3293
  }
2854
- normalizeEndpoint(value) {
2855
- return value.replace(/\/+$/, "");
3294
+ getResponseLengthInstruction() {
3295
+ if (!this._responseLength) {
3296
+ return void 0;
3297
+ }
3298
+ const maxTokens = MAX_TOKENS_BY_LENGTH[this._responseLength];
3299
+ if (!maxTokens) {
3300
+ return void 0;
3301
+ }
3302
+ return `Please keep your response concise, within approximately ${maxTokens} tokens.`;
2856
3303
  }
2857
3304
  };
2858
3305
 
2859
- // src/services/providers/openai/responsesParser.ts
2860
- async function parseOpenAIResponsesStream(res, onPartial) {
2861
- const reader = res.body.getReader();
2862
- const dec = new TextDecoder();
2863
- const textBlocks = [];
2864
- const toolCallsMap = /* @__PURE__ */ new Map();
2865
- let responseStatus;
2866
- let incompleteDetails;
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
- }
3306
+ // src/services/providers/geminiNano/GeminiNanoChatServiceProvider.ts
3307
+ var GeminiNanoChatServiceProvider = class {
3308
+ createChatService(options) {
3309
+ return new GeminiNanoChatService({
3310
+ expectedInputLanguages: options.expectedInputLanguages,
3311
+ expectedOutputLanguages: options.expectedOutputLanguages,
3312
+ responseLength: options.responseLength
3313
+ });
2911
3314
  }
2912
- const toolBlocks = Array.from(toolCallsMap.values()).map(
2913
- (tool) => ({
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;
3315
+ getProviderName() {
3316
+ return "gemini-nano";
2976
3317
  }
2977
- }
2978
- function extractResponsesMetadata(data, fallbackStatus) {
2979
- const response = data?.response ?? data;
2980
- return {
2981
- responseStatus: response?.status ?? fallbackStatus,
2982
- incompleteDetails: response?.incomplete_details ?? null,
2983
- usage: response?.usage
2984
- };
2985
- }
2986
- function parseOpenAIResponsesOneShot(data) {
2987
- const blocks = [];
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
- });
3318
+ getSupportedModels() {
3319
+ return [MODEL_GEMINI_NANO];
3320
+ }
3321
+ getDefaultModel() {
3322
+ return MODEL_GEMINI_NANO;
3323
+ }
3324
+ supportsVision() {
3325
+ return false;
3326
+ }
3327
+ getVisionSupportLevel() {
3328
+ return "unsupported";
3006
3329
  }
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
3330
  };
3034
- var OpenAIChatService = class {
3331
+
3332
+ // src/services/providers/kimi/KimiChatService.ts
3333
+ var KimiChatService = class {
3035
3334
  /**
3036
3335
  * Constructor
3037
- * @param apiKey OpenAI API key
3336
+ * @param apiKey Kimi API key
3038
3337
  * @param model Name of the model to use
3039
3338
  * @param visionModel Name of the vision model
3040
3339
  */
3041
- 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) {
3042
- this.provider = provider;
3340
+ constructor(apiKey, model = MODEL_KIMI_K2_6, visionModel = MODEL_KIMI_K2_6, tools, endpoint = ENDPOINT_KIMI_CHAT_COMPLETIONS_API, responseLength, responseFormat, thinking) {
3341
+ /** Provider name */
3342
+ this.provider = "kimi";
3043
3343
  this.apiKey = apiKey;
3044
3344
  this.model = model;
3045
3345
  this.tools = tools || [];
3046
3346
  this.endpoint = endpoint;
3047
- this.mcpServers = mcpServers;
3048
3347
  this.responseLength = responseLength;
3049
- this.verbosity = verbosity;
3050
- this.reasoning_effort = reasoning_effort;
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
- }
3348
+ this.responseFormat = responseFormat;
3349
+ this.thinking = thinking ?? { type: "enabled" };
3057
3350
  this.visionModel = visionModel;
3058
3351
  }
3059
3352
  /**
3060
3353
  * Get the current model name
3061
- * @returns Model name
3062
3354
  */
3063
3355
  getModel() {
3064
3356
  return this.model;
3065
3357
  }
3066
3358
  /**
3067
3359
  * Get the current vision model name
3068
- * @returns Vision model name
3069
3360
  */
3070
3361
  getVisionModel() {
3071
3362
  return this.visionModel;
3072
3363
  }
3073
3364
  /**
3074
3365
  * 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
3366
  */
3079
3367
  async processChat(messages, onPartialResponse, onCompleteResponse) {
3080
3368
  await processChatWithOptionalTools({
3081
3369
  hasTools: this.tools.length > 0,
3082
3370
  runWithoutTools: async () => {
3083
- const res = await this.callOpenAI(messages, this.model, true);
3084
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
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
- }
3371
+ const res = await this.callKimi(messages, this.model, true);
3372
+ return this.handleStream(res, onPartialResponse);
3098
3373
  },
3099
3374
  runWithTools: () => this.chatOnce(messages, true, onPartialResponse),
3100
3375
  onCompleteResponse,
@@ -3103,68 +3378,43 @@ If it's in another language, summarize in that language.
3103
3378
  }
3104
3379
  /**
3105
3380
  * 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
3381
  */
3111
3382
  async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
3112
- try {
3113
- await processChatWithOptionalTools({
3114
- hasTools: this.tools.length > 0,
3115
- runWithoutTools: async () => {
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;
3383
+ if (!isKimiVisionModel(this.visionModel)) {
3384
+ throw new Error(
3385
+ `Model ${this.visionModel} does not support vision capabilities.`
3386
+ );
3142
3387
  }
3388
+ await processChatWithOptionalTools({
3389
+ hasTools: this.tools.length > 0,
3390
+ runWithoutTools: async () => {
3391
+ const res = await this.callKimi(messages, this.visionModel, true);
3392
+ return this.handleStream(res, onPartialResponse);
3393
+ },
3394
+ runWithTools: () => this.visionChatOnce(messages, true, onPartialResponse),
3395
+ onCompleteResponse,
3396
+ toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use visionChatOnce() loop when tools are enabled."
3397
+ });
3143
3398
  }
3144
3399
  /**
3145
3400
  * 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
3401
  */
3152
3402
  async chatOnce(messages, stream = true, onPartialResponse = () => {
3153
3403
  }, maxTokens) {
3154
- const res = await this.callOpenAI(messages, this.model, stream, maxTokens);
3404
+ const res = await this.callKimi(messages, this.model, stream, maxTokens);
3155
3405
  return this.parseResponse(res, stream, onPartialResponse);
3156
3406
  }
3157
3407
  /**
3158
3408
  * 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
3409
  */
3165
3410
  async visionChatOnce(messages, stream = false, onPartialResponse = () => {
3166
3411
  }, maxTokens) {
3167
- const res = await this.callOpenAI(
3412
+ if (!isKimiVisionModel(this.visionModel)) {
3413
+ throw new Error(
3414
+ `Model ${this.visionModel} does not support vision capabilities.`
3415
+ );
3416
+ }
3417
+ const res = await this.callKimi(
3168
3418
  messages,
3169
3419
  this.visionModel,
3170
3420
  stream,
@@ -3172,205 +3422,258 @@ If it's in another language, summarize in that language.
3172
3422
  );
3173
3423
  return this.parseResponse(res, stream, onPartialResponse);
3174
3424
  }
3175
- /**
3176
- * Parse response based on endpoint type
3177
- */
3178
3425
  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
3426
  return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
3184
3427
  }
3185
- async callOpenAI(messages, model, stream = false, maxTokens) {
3428
+ async callKimi(messages, model, stream = false, maxTokens) {
3186
3429
  const body = this.buildRequestBody(messages, model, stream, maxTokens);
3187
- const headers = {};
3188
- const shouldSendAuthorization = this.provider !== "openai-compatible" || this.apiKey.trim() !== "";
3189
- if (shouldSendAuthorization) {
3190
- headers.Authorization = `Bearer ${this.apiKey}`;
3191
- }
3192
- const res = await ChatServiceHttpClient.post(this.endpoint, body, headers);
3430
+ const res = await ChatServiceHttpClient.post(this.endpoint, body, {
3431
+ Authorization: `Bearer ${this.apiKey}`
3432
+ });
3193
3433
  return res;
3194
3434
  }
3195
3435
  /**
3196
- * Build request body based on the endpoint type
3436
+ * Build request body (OpenAI-compatible Chat Completions)
3437
+ */
3438
+ buildRequestBody(messages, model, stream, maxTokens) {
3439
+ const body = {
3440
+ model,
3441
+ stream,
3442
+ messages
3443
+ };
3444
+ const tokenLimit = maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength);
3445
+ if (tokenLimit !== void 0) {
3446
+ body.max_tokens = tokenLimit;
3447
+ }
3448
+ if (this.responseFormat) {
3449
+ body.response_format = this.responseFormat;
3450
+ }
3451
+ const effectiveThinking = this.tools.length > 0 ? { type: "disabled" } : this.thinking;
3452
+ if (effectiveThinking) {
3453
+ if (this.isSelfHostedEndpoint()) {
3454
+ if (effectiveThinking.type === "disabled") {
3455
+ body.chat_template_kwargs = { thinking: false };
3456
+ }
3457
+ } else {
3458
+ body.thinking = effectiveThinking;
3459
+ }
3460
+ }
3461
+ const tools = this.buildToolsDefinition();
3462
+ if (tools.length > 0) {
3463
+ body.tools = tools;
3464
+ body.tool_choice = "auto";
3465
+ }
3466
+ return body;
3467
+ }
3468
+ isSelfHostedEndpoint() {
3469
+ return this.normalizeEndpoint(this.endpoint) !== this.normalizeEndpoint(ENDPOINT_KIMI_CHAT_COMPLETIONS_API);
3470
+ }
3471
+ normalizeEndpoint(value) {
3472
+ return value.replace(/\/+$/, "");
3473
+ }
3474
+ buildToolsDefinition() {
3475
+ return buildOpenAICompatibleTools(this.tools, "chat-completions");
3476
+ }
3477
+ async handleStream(res, onPartial) {
3478
+ return parseOpenAICompatibleTextStream(res, onPartial, {
3479
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3480
+ });
3481
+ }
3482
+ /**
3483
+ * Parse streaming response with tool support
3484
+ */
3485
+ async parseStream(res, onPartial) {
3486
+ return parseOpenAICompatibleToolStream(res, onPartial, {
3487
+ onJsonError: (payload) => console.debug("Failed to parse SSE data:", payload)
3488
+ });
3489
+ }
3490
+ /**
3491
+ * Parse non-streaming response
3492
+ */
3493
+ parseOneShot(data) {
3494
+ return parseOpenAICompatibleOneShot(data);
3495
+ }
3496
+ };
3497
+
3498
+ // src/services/providers/kimi/KimiChatServiceProvider.ts
3499
+ var KimiChatServiceProvider = class {
3500
+ /**
3501
+ * Create a chat service instance
3502
+ */
3503
+ createChatService(options) {
3504
+ const endpoint = this.resolveEndpoint(options);
3505
+ const model = options.model || this.getDefaultModel();
3506
+ const visionModel = resolveVisionModel({
3507
+ model,
3508
+ visionModel: options.visionModel,
3509
+ defaultModel: this.getDefaultModel(),
3510
+ defaultVisionModel: this.getDefaultVisionModel(),
3511
+ supportsVisionForModel: (visionModel2) => this.supportsVisionForModel(visionModel2),
3512
+ validate: "explicit"
3513
+ });
3514
+ const tools = options.tools;
3515
+ const defaultThinking = options.thinking ?? { type: "enabled" };
3516
+ const thinking = tools && tools.length > 0 ? { type: "disabled" } : defaultThinking;
3517
+ return new KimiChatService(
3518
+ options.apiKey,
3519
+ model,
3520
+ visionModel,
3521
+ tools,
3522
+ endpoint,
3523
+ options.responseLength,
3524
+ options.responseFormat,
3525
+ thinking
3526
+ );
3527
+ }
3528
+ /**
3529
+ * Get the provider name
3530
+ */
3531
+ getProviderName() {
3532
+ return "kimi";
3533
+ }
3534
+ /**
3535
+ * Get the list of supported models
3197
3536
  */
3198
- buildRequestBody(messages, model, stream, maxTokens) {
3199
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
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
- );
3537
+ getSupportedModels() {
3538
+ return [MODEL_KIMI_K2_6, MODEL_KIMI_K2_5];
3274
3539
  }
3275
3540
  /**
3276
- * Validate MCP servers compatibility with the current endpoint
3541
+ * Get the default model
3277
3542
  */
3278
- validateMCPCompatibility() {
3279
- if (this.mcpServers.length > 0 && this.endpoint === ENDPOINT_OPENAI_CHAT_COMPLETIONS_API) {
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
- }
3543
+ getDefaultModel() {
3544
+ return MODEL_KIMI_K2_6;
3284
3545
  }
3285
3546
  /**
3286
- * Clean messages for Responses API (remove timestamp and other extra properties)
3547
+ * Get the default vision model
3287
3548
  */
3288
- cleanMessagesForResponsesAPI(messages) {
3289
- return messages.map((msg) => {
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
- });
3549
+ getDefaultVisionModel() {
3550
+ return MODEL_KIMI_K2_6;
3317
3551
  }
3318
3552
  /**
3319
- * Build tools definition based on the endpoint type
3553
+ * Check if this provider supports vision
3320
3554
  */
3321
- buildToolsDefinition() {
3322
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
3323
- const toolDefs = [];
3324
- if (this.tools.length > 0) {
3325
- toolDefs.push(
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;
3555
+ supportsVision() {
3556
+ return this.getVisionSupportLevel() !== "unsupported";
3557
+ }
3558
+ getVisionSupportLevel() {
3559
+ return "supported";
3336
3560
  }
3337
3561
  /**
3338
- * Build MCP tools definition for Responses API
3562
+ * Check if a specific model supports vision capabilities
3339
3563
  */
3340
- buildMCPToolsDefinition() {
3341
- return this.mcpServers.map((server) => {
3342
- const mcpDef = {
3343
- type: "mcp",
3344
- // Using 'mcp' as indicated by the error message
3345
- server_label: server.name,
3346
- // Use server_label as required by API
3347
- server_url: server.url
3348
- // Use server_url instead of url
3349
- };
3350
- if (server.require_approval) {
3351
- mcpDef.require_approval = server.require_approval;
3352
- }
3353
- if (server.tool_configuration?.allowed_tools) {
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
- };
3564
+ supportsVisionForModel(model) {
3565
+ return isKimiVisionModel(model);
3566
+ }
3567
+ getVisionSupportLevelForModel(model) {
3568
+ return this.supportsVisionForModel(model) ? "supported" : "unsupported";
3569
+ }
3570
+ resolveEndpoint(options) {
3571
+ if (options.endpoint) {
3572
+ return this.normalizeEndpoint(options.endpoint);
3573
+ }
3574
+ if (options.baseUrl) {
3575
+ const baseUrl = this.normalizeEndpoint(options.baseUrl);
3576
+ if (baseUrl.endsWith("/chat/completions")) {
3577
+ return baseUrl;
3360
3578
  }
3361
- return mcpDef;
3362
- });
3579
+ return `${baseUrl}/chat/completions`;
3580
+ }
3581
+ return ENDPOINT_KIMI_CHAT_COMPLETIONS_API;
3363
3582
  }
3364
- async handleStream(res, onPartial) {
3365
- return parseOpenAICompatibleTextStream(res, onPartial);
3583
+ normalizeEndpoint(value) {
3584
+ return value.replace(/\/+$/, "");
3366
3585
  }
3367
- async parseStream(res, onPartial) {
3368
- return parseOpenAICompatibleToolStream(res, onPartial, {
3369
- appendTextBlock: StreamTextAccumulator.addTextBlock
3586
+ };
3587
+
3588
+ // src/services/providers/mistral/MistralChatService.ts
3589
+ var MistralChatService = class extends OpenAIChatService {
3590
+ constructor(apiKey, model = MODEL_MISTRAL_SMALL_LATEST, visionModel = model, tools, endpoint = ENDPOINT_MISTRAL_CHAT_COMPLETIONS_API, responseLength, reasoningEffort) {
3591
+ super(
3592
+ apiKey,
3593
+ model,
3594
+ visionModel,
3595
+ tools,
3596
+ endpoint,
3597
+ [],
3598
+ responseLength,
3599
+ void 0,
3600
+ reasoningEffort,
3601
+ false,
3602
+ "mistral",
3603
+ false
3604
+ );
3605
+ }
3606
+ };
3607
+
3608
+ // src/services/providers/mistral/MistralChatServiceProvider.ts
3609
+ var MistralChatServiceProvider = class {
3610
+ createChatService(options) {
3611
+ this.validateRequiredOptions(options);
3612
+ const model = options.model || this.getDefaultModel();
3613
+ const visionModel = resolveVisionModel({
3614
+ model,
3615
+ visionModel: options.visionModel,
3616
+ defaultModel: this.getDefaultModel(),
3617
+ defaultVisionModel: this.getDefaultVisionModel(),
3618
+ supportsVisionForModel: (targetModel) => this.supportsVisionForModel(targetModel),
3619
+ validate: "explicit"
3370
3620
  });
3621
+ const tools = options.tools;
3622
+ const reasoningEffort = isMistralReasoningEffortModel(model) ? options.reasoning_effort : void 0;
3623
+ return new MistralChatService(
3624
+ options.apiKey,
3625
+ model,
3626
+ visionModel,
3627
+ tools,
3628
+ this.resolveEndpoint(options),
3629
+ options.responseLength,
3630
+ reasoningEffort
3631
+ );
3371
3632
  }
3372
- parseOneShot(data) {
3373
- return parseOpenAICompatibleOneShot(data);
3633
+ getProviderName() {
3634
+ return "mistral";
3635
+ }
3636
+ getSupportedModels() {
3637
+ return [...MISTRAL_SUPPORTED_MODELS];
3638
+ }
3639
+ getDefaultModel() {
3640
+ return MODEL_MISTRAL_SMALL_LATEST;
3641
+ }
3642
+ getDefaultVisionModel() {
3643
+ return MODEL_MISTRAL_SMALL_LATEST;
3644
+ }
3645
+ supportsVision() {
3646
+ return this.getVisionSupportLevel() !== "unsupported";
3647
+ }
3648
+ getVisionSupportLevel() {
3649
+ return "supported";
3650
+ }
3651
+ supportsVisionForModel(model) {
3652
+ return isMistralVisionModel(model);
3653
+ }
3654
+ getVisionSupportLevelForModel(model) {
3655
+ return this.supportsVisionForModel(model) ? "supported" : "unsupported";
3656
+ }
3657
+ validateRequiredOptions(options) {
3658
+ if (!options.apiKey?.trim()) {
3659
+ throw new Error("mistral provider requires apiKey.");
3660
+ }
3661
+ }
3662
+ resolveEndpoint(options) {
3663
+ if (options.endpoint) {
3664
+ return this.normalizeUrl(options.endpoint);
3665
+ }
3666
+ if (options.baseUrl) {
3667
+ const baseUrl = this.normalizeUrl(options.baseUrl);
3668
+ if (baseUrl.endsWith("/chat/completions")) {
3669
+ return baseUrl;
3670
+ }
3671
+ return `${baseUrl}/chat/completions`;
3672
+ }
3673
+ return ENDPOINT_MISTRAL_CHAT_COMPLETIONS_API;
3674
+ }
3675
+ normalizeUrl(value) {
3676
+ return value.trim().replace(/\/+$/, "");
3374
3677
  }
3375
3678
  };
3376
3679
 
@@ -4447,7 +4750,9 @@ If it's in another language, summarize in that language.
4447
4750
  new OpenRouterChatServiceProvider(),
4448
4751
  new ZAIChatServiceProvider(),
4449
4752
  new XAIChatServiceProvider(),
4450
- new KimiChatServiceProvider()
4753
+ new KimiChatServiceProvider(),
4754
+ new DeepSeekChatServiceProvider(),
4755
+ new MistralChatServiceProvider()
4451
4756
  ];
4452
4757
 
4453
4758
  // src/services/ChatServiceFactory.ts