@aituber-onair/chat 0.33.0 → 0.34.1

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