@aituber-onair/chat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/README.ja.md +318 -0
  2. package/README.md +318 -0
  3. package/dist/cjs/constants/chat.d.ts +26 -0
  4. package/dist/cjs/constants/chat.d.ts.map +1 -0
  5. package/dist/cjs/constants/chat.js +34 -0
  6. package/dist/cjs/constants/chat.js.map +1 -0
  7. package/dist/cjs/constants/claude.d.ts +9 -0
  8. package/dist/cjs/constants/claude.d.ts.map +1 -0
  9. package/dist/cjs/constants/claude.js +20 -0
  10. package/dist/cjs/constants/claude.js.map +1 -0
  11. package/dist/cjs/constants/gemini.d.ts +11 -0
  12. package/dist/cjs/constants/gemini.d.ts.map +1 -0
  13. package/dist/cjs/constants/gemini.js +26 -0
  14. package/dist/cjs/constants/gemini.js.map +1 -0
  15. package/dist/cjs/constants/index.d.ts +9 -0
  16. package/dist/cjs/constants/index.d.ts.map +1 -0
  17. package/dist/cjs/constants/index.js +25 -0
  18. package/dist/cjs/constants/index.js.map +1 -0
  19. package/dist/cjs/constants/openai.d.ts +13 -0
  20. package/dist/cjs/constants/openai.d.ts.map +1 -0
  21. package/dist/cjs/constants/openai.js +28 -0
  22. package/dist/cjs/constants/openai.js.map +1 -0
  23. package/dist/cjs/constants/prompts.d.ts +3 -0
  24. package/dist/cjs/constants/prompts.d.ts.map +1 -0
  25. package/dist/cjs/constants/prompts.js +16 -0
  26. package/dist/cjs/constants/prompts.js.map +1 -0
  27. package/dist/cjs/index.d.ts +17 -0
  28. package/dist/cjs/index.d.ts.map +1 -0
  29. package/dist/cjs/index.js +45 -0
  30. package/dist/cjs/index.js.map +1 -0
  31. package/dist/cjs/services/ChatService.d.ts +51 -0
  32. package/dist/cjs/services/ChatService.d.ts.map +1 -0
  33. package/dist/cjs/services/ChatService.js +3 -0
  34. package/dist/cjs/services/ChatService.js.map +1 -0
  35. package/dist/cjs/services/ChatServiceFactory.d.ts +39 -0
  36. package/dist/cjs/services/ChatServiceFactory.d.ts.map +1 -0
  37. package/dist/cjs/services/ChatServiceFactory.js +65 -0
  38. package/dist/cjs/services/ChatServiceFactory.js.map +1 -0
  39. package/dist/cjs/services/providers/ChatServiceProvider.d.ts +52 -0
  40. package/dist/cjs/services/providers/ChatServiceProvider.d.ts.map +1 -0
  41. package/dist/cjs/services/providers/ChatServiceProvider.js +3 -0
  42. package/dist/cjs/services/providers/ChatServiceProvider.js.map +1 -0
  43. package/dist/cjs/services/providers/claude/ClaudeChatService.d.ts +142 -0
  44. package/dist/cjs/services/providers/claude/ClaudeChatService.d.ts.map +1 -0
  45. package/dist/cjs/services/providers/claude/ClaudeChatService.js +501 -0
  46. package/dist/cjs/services/providers/claude/ClaudeChatService.js.map +1 -0
  47. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.d.ts +40 -0
  48. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.d.ts.map +1 -0
  49. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.js +68 -0
  50. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.js.map +1 -0
  51. package/dist/cjs/services/providers/gemini/GeminiChatService.d.ts +104 -0
  52. package/dist/cjs/services/providers/gemini/GeminiChatService.d.ts.map +1 -0
  53. package/dist/cjs/services/providers/gemini/GeminiChatService.js +653 -0
  54. package/dist/cjs/services/providers/gemini/GeminiChatService.js.map +1 -0
  55. package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.d.ts +40 -0
  56. package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.d.ts.map +1 -0
  57. package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.js +70 -0
  58. package/dist/cjs/services/providers/gemini/GeminiChatServiceProvider.js.map +1 -0
  59. package/dist/cjs/services/providers/openai/OpenAIChatService.d.ts +110 -0
  60. package/dist/cjs/services/providers/openai/OpenAIChatService.d.ts.map +1 -0
  61. package/dist/cjs/services/providers/openai/OpenAIChatService.js +544 -0
  62. package/dist/cjs/services/providers/openai/OpenAIChatService.js.map +1 -0
  63. package/dist/cjs/services/providers/openai/OpenAIChatServiceProvider.d.ts +40 -0
  64. package/dist/cjs/services/providers/openai/OpenAIChatServiceProvider.d.ts.map +1 -0
  65. package/dist/cjs/services/providers/openai/OpenAIChatServiceProvider.js +80 -0
  66. package/dist/cjs/services/providers/openai/OpenAIChatServiceProvider.js.map +1 -0
  67. package/dist/cjs/types/chat.d.ts +46 -0
  68. package/dist/cjs/types/chat.d.ts.map +1 -0
  69. package/dist/cjs/types/chat.js +6 -0
  70. package/dist/cjs/types/chat.js.map +1 -0
  71. package/dist/cjs/types/index.d.ts +8 -0
  72. package/dist/cjs/types/index.d.ts.map +1 -0
  73. package/dist/cjs/types/index.js +25 -0
  74. package/dist/cjs/types/index.js.map +1 -0
  75. package/dist/cjs/types/mcp.d.ts +37 -0
  76. package/dist/cjs/types/mcp.d.ts.map +1 -0
  77. package/dist/cjs/types/mcp.js +6 -0
  78. package/dist/cjs/types/mcp.js.map +1 -0
  79. package/dist/cjs/types/toolChat.d.ts +42 -0
  80. package/dist/cjs/types/toolChat.d.ts.map +1 -0
  81. package/dist/cjs/types/toolChat.js +3 -0
  82. package/dist/cjs/types/toolChat.js.map +1 -0
  83. package/dist/cjs/utils/chatServiceHttpClient.d.ts +47 -0
  84. package/dist/cjs/utils/chatServiceHttpClient.d.ts.map +1 -0
  85. package/dist/cjs/utils/chatServiceHttpClient.js +131 -0
  86. package/dist/cjs/utils/chatServiceHttpClient.js.map +1 -0
  87. package/dist/cjs/utils/emotionParser.d.ts +46 -0
  88. package/dist/cjs/utils/emotionParser.d.ts.map +1 -0
  89. package/dist/cjs/utils/emotionParser.js +59 -0
  90. package/dist/cjs/utils/emotionParser.js.map +1 -0
  91. package/dist/cjs/utils/index.d.ts +8 -0
  92. package/dist/cjs/utils/index.d.ts.map +1 -0
  93. package/dist/cjs/utils/index.js +24 -0
  94. package/dist/cjs/utils/index.js.map +1 -0
  95. package/dist/cjs/utils/mcpSchemaFetcher.d.ts +19 -0
  96. package/dist/cjs/utils/mcpSchemaFetcher.d.ts.map +1 -0
  97. package/dist/cjs/utils/mcpSchemaFetcher.js +98 -0
  98. package/dist/cjs/utils/mcpSchemaFetcher.js.map +1 -0
  99. package/dist/cjs/utils/screenplay.d.ts +20 -0
  100. package/dist/cjs/utils/screenplay.d.ts.map +1 -0
  101. package/dist/cjs/utils/screenplay.js +41 -0
  102. package/dist/cjs/utils/screenplay.js.map +1 -0
  103. package/dist/cjs/utils/streamTextAccumulator.d.ts +25 -0
  104. package/dist/cjs/utils/streamTextAccumulator.d.ts.map +1 -0
  105. package/dist/cjs/utils/streamTextAccumulator.js +47 -0
  106. package/dist/cjs/utils/streamTextAccumulator.js.map +1 -0
  107. package/dist/esm/constants/chat.d.ts +26 -0
  108. package/dist/esm/constants/chat.d.ts.map +1 -0
  109. package/dist/esm/constants/chat.js +30 -0
  110. package/dist/esm/constants/chat.js.map +1 -0
  111. package/dist/esm/constants/claude.d.ts +9 -0
  112. package/dist/esm/constants/claude.d.ts.map +1 -0
  113. package/dist/esm/constants/claude.js +17 -0
  114. package/dist/esm/constants/claude.js.map +1 -0
  115. package/dist/esm/constants/gemini.d.ts +11 -0
  116. package/dist/esm/constants/gemini.d.ts.map +1 -0
  117. package/dist/esm/constants/gemini.js +23 -0
  118. package/dist/esm/constants/gemini.js.map +1 -0
  119. package/dist/esm/constants/index.d.ts +9 -0
  120. package/dist/esm/constants/index.d.ts.map +1 -0
  121. package/dist/esm/constants/index.js +9 -0
  122. package/dist/esm/constants/index.js.map +1 -0
  123. package/dist/esm/constants/openai.d.ts +13 -0
  124. package/dist/esm/constants/openai.d.ts.map +1 -0
  125. package/dist/esm/constants/openai.js +25 -0
  126. package/dist/esm/constants/openai.js.map +1 -0
  127. package/dist/esm/constants/prompts.d.ts +3 -0
  128. package/dist/esm/constants/prompts.d.ts.map +1 -0
  129. package/dist/esm/constants/prompts.js +13 -0
  130. package/dist/esm/constants/prompts.js.map +1 -0
  131. package/dist/esm/index.d.ts +17 -0
  132. package/dist/esm/index.d.ts.map +1 -0
  133. package/dist/esm/index.js +21 -0
  134. package/dist/esm/index.js.map +1 -0
  135. package/dist/esm/services/ChatService.d.ts +51 -0
  136. package/dist/esm/services/ChatService.d.ts.map +1 -0
  137. package/dist/esm/services/ChatService.js +2 -0
  138. package/dist/esm/services/ChatService.js.map +1 -0
  139. package/dist/esm/services/ChatServiceFactory.d.ts +39 -0
  140. package/dist/esm/services/ChatServiceFactory.d.ts.map +1 -0
  141. package/dist/esm/services/ChatServiceFactory.js +61 -0
  142. package/dist/esm/services/ChatServiceFactory.js.map +1 -0
  143. package/dist/esm/services/providers/ChatServiceProvider.d.ts +52 -0
  144. package/dist/esm/services/providers/ChatServiceProvider.d.ts.map +1 -0
  145. package/dist/esm/services/providers/ChatServiceProvider.js +2 -0
  146. package/dist/esm/services/providers/ChatServiceProvider.js.map +1 -0
  147. package/dist/esm/services/providers/claude/ClaudeChatService.d.ts +142 -0
  148. package/dist/esm/services/providers/claude/ClaudeChatService.d.ts.map +1 -0
  149. package/dist/esm/services/providers/claude/ClaudeChatService.js +497 -0
  150. package/dist/esm/services/providers/claude/ClaudeChatService.js.map +1 -0
  151. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.d.ts +40 -0
  152. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.d.ts.map +1 -0
  153. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.js +64 -0
  154. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.js.map +1 -0
  155. package/dist/esm/services/providers/gemini/GeminiChatService.d.ts +104 -0
  156. package/dist/esm/services/providers/gemini/GeminiChatService.d.ts.map +1 -0
  157. package/dist/esm/services/providers/gemini/GeminiChatService.js +649 -0
  158. package/dist/esm/services/providers/gemini/GeminiChatService.js.map +1 -0
  159. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.d.ts +40 -0
  160. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.d.ts.map +1 -0
  161. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.js +66 -0
  162. package/dist/esm/services/providers/gemini/GeminiChatServiceProvider.js.map +1 -0
  163. package/dist/esm/services/providers/openai/OpenAIChatService.d.ts +110 -0
  164. package/dist/esm/services/providers/openai/OpenAIChatService.d.ts.map +1 -0
  165. package/dist/esm/services/providers/openai/OpenAIChatService.js +540 -0
  166. package/dist/esm/services/providers/openai/OpenAIChatService.js.map +1 -0
  167. package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.d.ts +40 -0
  168. package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.d.ts.map +1 -0
  169. package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.js +76 -0
  170. package/dist/esm/services/providers/openai/OpenAIChatServiceProvider.js.map +1 -0
  171. package/dist/esm/types/chat.d.ts +46 -0
  172. package/dist/esm/types/chat.d.ts.map +1 -0
  173. package/dist/esm/types/chat.js +5 -0
  174. package/dist/esm/types/chat.js.map +1 -0
  175. package/dist/esm/types/index.d.ts +8 -0
  176. package/dist/esm/types/index.d.ts.map +1 -0
  177. package/dist/esm/types/index.js +9 -0
  178. package/dist/esm/types/index.js.map +1 -0
  179. package/dist/esm/types/mcp.d.ts +37 -0
  180. package/dist/esm/types/mcp.d.ts.map +1 -0
  181. package/dist/esm/types/mcp.js +5 -0
  182. package/dist/esm/types/mcp.js.map +1 -0
  183. package/dist/esm/types/toolChat.d.ts +42 -0
  184. package/dist/esm/types/toolChat.d.ts.map +1 -0
  185. package/dist/esm/types/toolChat.js +2 -0
  186. package/dist/esm/types/toolChat.js.map +1 -0
  187. package/dist/esm/utils/chatServiceHttpClient.d.ts +47 -0
  188. package/dist/esm/utils/chatServiceHttpClient.d.ts.map +1 -0
  189. package/dist/esm/utils/chatServiceHttpClient.js +126 -0
  190. package/dist/esm/utils/chatServiceHttpClient.js.map +1 -0
  191. package/dist/esm/utils/emotionParser.d.ts +46 -0
  192. package/dist/esm/utils/emotionParser.d.ts.map +1 -0
  193. package/dist/esm/utils/emotionParser.js +55 -0
  194. package/dist/esm/utils/emotionParser.js.map +1 -0
  195. package/dist/esm/utils/index.d.ts +8 -0
  196. package/dist/esm/utils/index.d.ts.map +1 -0
  197. package/dist/esm/utils/index.js +8 -0
  198. package/dist/esm/utils/index.js.map +1 -0
  199. package/dist/esm/utils/mcpSchemaFetcher.d.ts +19 -0
  200. package/dist/esm/utils/mcpSchemaFetcher.d.ts.map +1 -0
  201. package/dist/esm/utils/mcpSchemaFetcher.js +94 -0
  202. package/dist/esm/utils/mcpSchemaFetcher.js.map +1 -0
  203. package/dist/esm/utils/screenplay.d.ts +20 -0
  204. package/dist/esm/utils/screenplay.d.ts.map +1 -0
  205. package/dist/esm/utils/screenplay.js +36 -0
  206. package/dist/esm/utils/screenplay.js.map +1 -0
  207. package/dist/esm/utils/streamTextAccumulator.d.ts +25 -0
  208. package/dist/esm/utils/streamTextAccumulator.d.ts.map +1 -0
  209. package/dist/esm/utils/streamTextAccumulator.js +43 -0
  210. package/dist/esm/utils/streamTextAccumulator.js.map +1 -0
  211. package/package.json +54 -0
@@ -0,0 +1,649 @@
1
+ import { ENDPOINT_GEMINI_API, MODEL_GEMINI_2_0_FLASH_LITE, GEMINI_VISION_SUPPORTED_MODELS, } from '../../../constants';
2
+ import { getMaxTokensForResponseLength, } from '../../../constants/chat';
3
+ import { StreamTextAccumulator } from '../../../utils/streamTextAccumulator';
4
+ import { ChatServiceHttpClient } from '../../../utils/chatServiceHttpClient';
5
+ import { MCPSchemaFetcher } from '../../../utils/mcpSchemaFetcher';
6
+ /**
7
+ * Gemini implementation of ChatService
8
+ */
9
+ export class GeminiChatService {
10
+ /* ────────────────────────────────── */
11
+ /* Utilities */
12
+ /* ────────────────────────────────── */
13
+ safeJsonParse(str) {
14
+ try {
15
+ return JSON.parse(str);
16
+ }
17
+ catch {
18
+ return str; // keep as string
19
+ }
20
+ }
21
+ normalizeToolResult(val) {
22
+ if (val === null)
23
+ return { content: null };
24
+ if (typeof val === 'object')
25
+ return val;
26
+ return { content: val }; // wrap primitive
27
+ }
28
+ /**
29
+ * camelCase → snake_case conversion (v1beta)
30
+ */
31
+ adaptKeysForApi(obj) {
32
+ const map = {
33
+ toolConfig: 'tool_config',
34
+ functionCallingConfig: 'function_calling_config',
35
+ functionDeclarations: 'function_declarations',
36
+ functionCall: 'function_call',
37
+ functionResponse: 'function_response',
38
+ };
39
+ if (Array.isArray(obj))
40
+ return obj.map((v) => this.adaptKeysForApi(v));
41
+ if (obj && typeof obj === 'object') {
42
+ return Object.fromEntries(Object.entries(obj).map(([k, v]) => [
43
+ map[k] ?? k,
44
+ this.adaptKeysForApi(v),
45
+ ]));
46
+ }
47
+ return obj;
48
+ }
49
+ /**
50
+ * Constructor
51
+ * @param apiKey Google API key
52
+ * @param model Name of the model to use
53
+ * @param visionModel Name of the vision model
54
+ * @param tools Array of tool definitions
55
+ * @param mcpServers Array of MCP server configurations
56
+ */
57
+ constructor(apiKey, model = MODEL_GEMINI_2_0_FLASH_LITE, visionModel = MODEL_GEMINI_2_0_FLASH_LITE, tools = [], mcpServers = [], responseLength) {
58
+ /** Provider name */
59
+ this.provider = 'gemini';
60
+ this.mcpToolSchemas = [];
61
+ this.mcpSchemasInitialized = false;
62
+ /** id(OpenAI) → name(Gemini) mapping */
63
+ this.callIdMap = new Map();
64
+ this.apiKey = apiKey;
65
+ this.model = model;
66
+ this.responseLength = responseLength;
67
+ // check if the vision model is supported
68
+ if (!GEMINI_VISION_SUPPORTED_MODELS.includes(visionModel)) {
69
+ throw new Error(`Model ${visionModel} does not support vision capabilities.`);
70
+ }
71
+ this.visionModel = visionModel;
72
+ this.tools = tools;
73
+ this.mcpServers = mcpServers;
74
+ }
75
+ /**
76
+ * Get the current model name
77
+ * @returns Model name
78
+ */
79
+ getModel() {
80
+ return this.model;
81
+ }
82
+ /**
83
+ * Get the current vision model name
84
+ * @returns Vision model name
85
+ */
86
+ getVisionModel() {
87
+ return this.visionModel;
88
+ }
89
+ /**
90
+ * Get configured MCP servers
91
+ * @returns Array of MCP server configurations
92
+ */
93
+ getMCPServers() {
94
+ return this.mcpServers;
95
+ }
96
+ /**
97
+ * Add MCP server configuration
98
+ * @param serverConfig MCP server configuration
99
+ */
100
+ addMCPServer(serverConfig) {
101
+ this.mcpServers.push(serverConfig);
102
+ // Reset initialization flag to re-fetch schemas
103
+ this.mcpSchemasInitialized = false;
104
+ }
105
+ /**
106
+ * Remove MCP server by name
107
+ * @param serverName Name of the server to remove
108
+ */
109
+ removeMCPServer(serverName) {
110
+ this.mcpServers = this.mcpServers.filter((server) => server.name !== serverName);
111
+ // Reset initialization flag to re-fetch schemas
112
+ this.mcpSchemasInitialized = false;
113
+ }
114
+ /**
115
+ * Check if MCP servers are configured
116
+ * @returns True if MCP servers are configured
117
+ */
118
+ hasMCPServers() {
119
+ return this.mcpServers.length > 0;
120
+ }
121
+ /**
122
+ * Initialize MCP tool schemas by fetching from servers
123
+ * @private
124
+ */
125
+ async initializeMCPSchemas() {
126
+ if (this.mcpSchemasInitialized || this.mcpServers.length === 0) {
127
+ return;
128
+ }
129
+ try {
130
+ // Add timeout to prevent hanging
131
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('MCP schema fetch timeout')), 5000));
132
+ const schemasPromise = MCPSchemaFetcher.fetchAllToolSchemas(this.mcpServers);
133
+ this.mcpToolSchemas = await Promise.race([
134
+ schemasPromise,
135
+ timeoutPromise,
136
+ ]);
137
+ this.mcpSchemasInitialized = true;
138
+ }
139
+ catch (error) {
140
+ console.warn('Failed to initialize MCP schemas, using fallback:', error);
141
+ // Use fallback schemas - always provide basic functionality
142
+ this.mcpToolSchemas = this.mcpServers.map((server) => ({
143
+ name: `mcp_${server.name}_search`,
144
+ description: `Search using ${server.name} MCP server (fallback)`,
145
+ parameters: {
146
+ type: 'object',
147
+ properties: {
148
+ query: {
149
+ type: 'string',
150
+ description: 'Search query',
151
+ },
152
+ },
153
+ required: ['query'],
154
+ },
155
+ }));
156
+ this.mcpSchemasInitialized = true;
157
+ }
158
+ }
159
+ /**
160
+ * Process chat messages
161
+ * @param messages Array of messages to send
162
+ * @param onPartialResponse Callback to receive each part of streaming response
163
+ * @param onCompleteResponse Callback to execute when response is complete
164
+ */
165
+ async processChat(messages, onPartialResponse, onCompleteResponse) {
166
+ try {
167
+ // not use tools or MCP servers
168
+ if (this.tools.length === 0 && this.mcpServers.length === 0) {
169
+ const res = await this.callGemini(messages, this.model, true);
170
+ const { blocks } = await this.parseStream(res, onPartialResponse);
171
+ const full = blocks
172
+ .filter((b) => b.type === 'text')
173
+ .map((b) => b.text)
174
+ .join('');
175
+ await onCompleteResponse(full);
176
+ return;
177
+ }
178
+ /* with tools (1 turn) */
179
+ const { blocks, stop_reason } = await this.chatOnce(messages, true, onPartialResponse);
180
+ if (stop_reason === 'end') {
181
+ const full = blocks
182
+ .filter((b) => b.type === 'text')
183
+ .map((b) => b.text)
184
+ .join('');
185
+ await onCompleteResponse(full);
186
+ return;
187
+ }
188
+ throw new Error('Received functionCall. Use chatOnce() loop when tools are enabled.');
189
+ }
190
+ catch (err) {
191
+ console.error('Error in processChat:', err);
192
+ throw err;
193
+ }
194
+ }
195
+ async processVisionChat(messages, onPartialResponse, onCompleteResponse) {
196
+ try {
197
+ if (this.tools.length === 0 && this.mcpServers.length === 0) {
198
+ const res = await this.callGemini(messages, this.visionModel, true);
199
+ const { blocks } = await this.parseStream(res, onPartialResponse);
200
+ const full = blocks
201
+ .filter((b) => b.type === 'text')
202
+ .map((b) => b.text)
203
+ .join('');
204
+ await onCompleteResponse(full);
205
+ return;
206
+ }
207
+ const { blocks, stop_reason } = await this.visionChatOnce(messages);
208
+ blocks
209
+ .filter((b) => b.type === 'text')
210
+ .forEach((b) => onPartialResponse(b.text));
211
+ if (stop_reason === 'end') {
212
+ const full = blocks
213
+ .filter((b) => b.type === 'text')
214
+ .map((b) => b.text)
215
+ .join('');
216
+ await onCompleteResponse(full);
217
+ return;
218
+ }
219
+ throw new Error('Received functionCall. Use visionChatOnce() loop when tools are enabled.');
220
+ }
221
+ catch (err) {
222
+ console.error('Error in processVisionChat:', err);
223
+ throw err;
224
+ }
225
+ }
226
+ /* ────────────────────────────────── */
227
+ /* OpenAI → Gemini conversion */
228
+ /* ────────────────────────────────── */
229
+ convertMessagesToGeminiFormat(messages) {
230
+ const gemini = [];
231
+ let currentRole = null;
232
+ let currentParts = [];
233
+ const pushCurrent = () => {
234
+ if (currentRole && currentParts.length) {
235
+ gemini.push({ role: currentRole, parts: [...currentParts] });
236
+ currentParts = [];
237
+ }
238
+ };
239
+ for (const msg of messages) {
240
+ const role = this.mapRoleToGemini(msg.role);
241
+ /* assistant: tool_calls -> functionCall */
242
+ if (msg.tool_calls) {
243
+ pushCurrent();
244
+ for (const call of msg.tool_calls) {
245
+ this.callIdMap.set(call.id, call.function.name);
246
+ gemini.push({
247
+ role: 'model',
248
+ parts: [
249
+ {
250
+ functionCall: {
251
+ name: call.function.name,
252
+ args: JSON.parse(call.function.arguments || '{}'),
253
+ },
254
+ },
255
+ ],
256
+ });
257
+ }
258
+ continue;
259
+ }
260
+ /* tool → functionResponse */
261
+ if (msg.role === 'tool') {
262
+ pushCurrent();
263
+ const funcName = msg.name ??
264
+ this.callIdMap.get(msg.tool_call_id) ??
265
+ 'result';
266
+ gemini.push({
267
+ role: 'user',
268
+ parts: [
269
+ {
270
+ functionResponse: {
271
+ name: funcName,
272
+ response: this.normalizeToolResult(this.safeJsonParse(msg.content)),
273
+ },
274
+ },
275
+ ],
276
+ });
277
+ continue;
278
+ }
279
+ /* normal text */
280
+ if (role !== currentRole)
281
+ pushCurrent();
282
+ currentRole = role;
283
+ currentParts.push({ text: msg.content });
284
+ }
285
+ pushCurrent();
286
+ return gemini;
287
+ }
288
+ /* ────────────────────────────────── */
289
+ /* HTTP call */
290
+ /* ────────────────────────────────── */
291
+ async callGemini(messages, model, stream = false, maxTokens) {
292
+ const hasVision = messages.some((m) => Array.isArray(m.content) &&
293
+ m.content.some((b) => b?.type === 'image_url' || b?.inlineData));
294
+ const contents = hasVision
295
+ ? await this.convertVisionMessagesToGeminiFormat(messages)
296
+ : this.convertMessagesToGeminiFormat(messages);
297
+ const body = {
298
+ contents,
299
+ generationConfig: {
300
+ maxOutputTokens: maxTokens !== undefined
301
+ ? maxTokens
302
+ : getMaxTokensForResponseLength(this.responseLength),
303
+ },
304
+ };
305
+ // Add tools configuration (regular tools + MCP tools as functionDeclarations)
306
+ const allToolDeclarations = [];
307
+ // Add regular function tools
308
+ if (this.tools.length > 0) {
309
+ allToolDeclarations.push(...this.tools.map((t) => ({
310
+ name: t.name,
311
+ description: t.description,
312
+ parameters: t.parameters,
313
+ })));
314
+ }
315
+ // Add MCP tools as functionDeclarations
316
+ if (this.mcpServers.length > 0) {
317
+ try {
318
+ // Initialize MCP schemas if not already done
319
+ await this.initializeMCPSchemas();
320
+ // Add MCP tool schemas as regular function declarations
321
+ // Gemini will call these as normal functions, and ToolExecutor will handle the MCP routing
322
+ allToolDeclarations.push(...this.mcpToolSchemas.map((t) => ({
323
+ name: t.name,
324
+ description: t.description,
325
+ parameters: t.parameters,
326
+ })));
327
+ }
328
+ catch (error) {
329
+ console.warn('MCP initialization failed, skipping MCP tools:', error);
330
+ // Continue without MCP tools if initialization fails
331
+ }
332
+ }
333
+ if (allToolDeclarations.length > 0) {
334
+ body.tools = [
335
+ {
336
+ functionDeclarations: allToolDeclarations,
337
+ },
338
+ ];
339
+ body.toolConfig = { functionCallingConfig: { mode: 'AUTO' } };
340
+ }
341
+ const fetchOnce = async (ver, payload) => {
342
+ const fn = stream ? 'streamGenerateContent' : 'generateContent';
343
+ const alt = stream ? '?alt=sse' : '';
344
+ const url = `${ENDPOINT_GEMINI_API}/${ver}/models/${model}:${fn}${alt}${alt ? '&' : '?'}key=${this.apiKey}`;
345
+ return ChatServiceHttpClient.post(url, payload);
346
+ };
347
+ const isLite = /flash[-_]lite/.test(model);
348
+ const isGemini25 = /gemini-2\.5/.test(model);
349
+ const firstVer = isLite || isGemini25 ? 'v1beta' : 'v1';
350
+ const tryApi = async () => {
351
+ try {
352
+ const payload = firstVer === 'v1' ? body : this.adaptKeysForApi(body); // snake_case conversion
353
+ return await fetchOnce(firstVer, payload);
354
+ }
355
+ catch (e) {
356
+ // Only retry v1beta if camel/snake case mismatch error occurs in models that don't require v1beta
357
+ if (!(isLite || isGemini25) &&
358
+ /Unknown name|Cannot find field|404/.test(e.message)) {
359
+ return await fetchOnce('v1beta', this.adaptKeysForApi(body));
360
+ }
361
+ throw e; // otherwise, throw to upper layer
362
+ }
363
+ };
364
+ try {
365
+ const res = await tryApi();
366
+ return res;
367
+ }
368
+ catch (error) {
369
+ // Enhanced error logging for debugging
370
+ if (error.body) {
371
+ console.error('Gemini API Error Details:', error.body);
372
+ console.error('Request Body:', JSON.stringify(body, null, 2));
373
+ }
374
+ throw error;
375
+ }
376
+ }
377
+ /**
378
+ * Convert AITuber OnAir vision messages to Gemini format
379
+ * @param messages Array of vision messages
380
+ * @returns Gemini formatted vision messages
381
+ */
382
+ async convertVisionMessagesToGeminiFormat(messages) {
383
+ const geminiMessages = [];
384
+ let currentRole = null;
385
+ let currentParts = [];
386
+ for (const msg of messages) {
387
+ // Map AITuber OnAir roles to Gemini roles
388
+ const role = this.mapRoleToGemini(msg.role);
389
+ /* ----------- OpenAI compatible tool metadata ----------- */
390
+ // assistant: { tool_calls:[{id,name,function:{arguments}}] }
391
+ if (msg.tool_calls) {
392
+ for (const call of msg.tool_calls) {
393
+ // Gemini does not need id. Insert functionCall into parts
394
+ geminiMessages.push({
395
+ role: 'model',
396
+ parts: [
397
+ {
398
+ functionCall: {
399
+ name: call.function.name,
400
+ args: JSON.parse(call.function.arguments || '{}'),
401
+ },
402
+ },
403
+ ],
404
+ });
405
+ }
406
+ continue;
407
+ }
408
+ // tool role → user role + functionResponse
409
+ if (msg.role === 'tool') {
410
+ const funcName = msg.name ??
411
+ this.callIdMap.get(msg.tool_call_id) ??
412
+ 'result';
413
+ geminiMessages.push({
414
+ role: 'user',
415
+ parts: [
416
+ {
417
+ functionResponse: {
418
+ name: funcName,
419
+ response: this.normalizeToolResult(this.safeJsonParse(msg.content)),
420
+ },
421
+ },
422
+ ],
423
+ });
424
+ continue;
425
+ }
426
+ // If role changes, start a new message
427
+ if (role !== currentRole && currentParts.length > 0) {
428
+ geminiMessages.push({
429
+ role: currentRole,
430
+ parts: [...currentParts],
431
+ });
432
+ currentParts = [];
433
+ }
434
+ currentRole = role;
435
+ // If the message has content blocks, process them
436
+ if (typeof msg.content === 'string') {
437
+ currentParts.push({ text: msg.content });
438
+ }
439
+ else if (Array.isArray(msg.content)) {
440
+ // Process each content block (text or image)
441
+ for (const block of msg.content) {
442
+ if (block.type === 'text') {
443
+ currentParts.push({ text: block.text });
444
+ }
445
+ else if (block.type === 'image_url') {
446
+ try {
447
+ // Fetch the image data from URL
448
+ const imageResponse = await ChatServiceHttpClient.get(block.image_url.url);
449
+ // Convert image to blob and then to base64
450
+ const imageBlob = await imageResponse.blob();
451
+ const base64Data = await this.blobToBase64(imageBlob);
452
+ // Add image data in Gemini format
453
+ currentParts.push({
454
+ inlineData: {
455
+ mimeType: imageBlob.type || 'image/jpeg',
456
+ data: base64Data.split(',')[1], // Remove the "data:image/jpeg;base64," prefix
457
+ },
458
+ });
459
+ }
460
+ catch (error) {
461
+ console.error('Error processing image:', error);
462
+ throw new Error(`Failed to process image: ${error.message}`);
463
+ }
464
+ }
465
+ }
466
+ }
467
+ }
468
+ // Add the last message
469
+ if (currentRole && currentParts.length > 0) {
470
+ geminiMessages.push({
471
+ role: currentRole,
472
+ parts: [...currentParts],
473
+ });
474
+ }
475
+ return geminiMessages;
476
+ }
477
+ /**
478
+ * Convert Blob to Base64 string
479
+ * @param blob Image blob
480
+ * @returns Promise with base64 encoded string
481
+ */
482
+ blobToBase64(blob) {
483
+ return new Promise((resolve, reject) => {
484
+ const reader = new FileReader();
485
+ reader.onloadend = () => resolve(reader.result);
486
+ reader.onerror = reject;
487
+ reader.readAsDataURL(blob);
488
+ });
489
+ }
490
+ /**
491
+ * Map AITuber OnAir roles to Gemini roles
492
+ * @param role AITuber OnAir role
493
+ * @returns Gemini role
494
+ */
495
+ mapRoleToGemini(role) {
496
+ switch (role) {
497
+ case 'system':
498
+ return 'model'; // Gemini uses 'model' for system messages
499
+ case 'user':
500
+ return 'user';
501
+ case 'assistant':
502
+ return 'model';
503
+ default:
504
+ return 'user';
505
+ }
506
+ }
507
+ /* ────────────────────────────────────────────────────────── */
508
+ /* Convert NDJSON stream to common format */
509
+ /* ────────────────────────────────────────────────────────── */
510
+ async parseStream(res, onPartial) {
511
+ const reader = res.body.getReader();
512
+ const dec = new TextDecoder();
513
+ const textBlocks = [];
514
+ const toolBlocks = [];
515
+ let buf = '';
516
+ const flush = (payload) => {
517
+ if (!payload || payload === '[DONE]')
518
+ return;
519
+ let obj;
520
+ try {
521
+ obj = JSON.parse(payload);
522
+ }
523
+ catch {
524
+ return;
525
+ }
526
+ for (const cand of obj.candidates ?? []) {
527
+ for (const part of cand.content?.parts ?? []) {
528
+ if (part.text) {
529
+ onPartial(part.text);
530
+ StreamTextAccumulator.addTextBlock(textBlocks, part.text);
531
+ }
532
+ if (part.functionCall) {
533
+ toolBlocks.push({
534
+ type: 'tool_use',
535
+ id: this.genUUID(),
536
+ name: part.functionCall.name,
537
+ input: part.functionCall.args ?? {},
538
+ });
539
+ }
540
+ if (part.functionResponse) {
541
+ toolBlocks.push({
542
+ type: 'tool_result',
543
+ tool_use_id: part.functionResponse.name,
544
+ content: JSON.stringify(part.functionResponse.response),
545
+ });
546
+ }
547
+ }
548
+ }
549
+ };
550
+ while (true) {
551
+ const { done, value } = await reader.read();
552
+ if (done)
553
+ break;
554
+ buf += dec.decode(value, { stream: true });
555
+ let nl;
556
+ while ((nl = buf.indexOf('\n')) !== -1) {
557
+ let line = buf.slice(0, nl);
558
+ buf = buf.slice(nl + 1);
559
+ if (line.endsWith('\r'))
560
+ line = line.slice(0, -1); // CRLF support
561
+ if (!line.trim()) {
562
+ flush('');
563
+ continue;
564
+ } // keep-alive empty line
565
+ if (line.startsWith('data:'))
566
+ line = line.slice(5).trim();
567
+ if (!line)
568
+ continue;
569
+ flush(line);
570
+ }
571
+ }
572
+ if (buf)
573
+ flush(buf);
574
+ const blocks = [...textBlocks, ...toolBlocks];
575
+ return {
576
+ blocks,
577
+ stop_reason: toolBlocks.some((b) => b.type === 'tool_use')
578
+ ? 'tool_use'
579
+ : 'end',
580
+ };
581
+ }
582
+ /* ────────────────────────────────────────────────────────── */
583
+ /* Convert JSON of non-stream (= generateContent) */
584
+ /* ────────────────────────────────────────────────────────── */
585
+ parseOneShot(data) {
586
+ const textBlocks = [];
587
+ const toolBlocks = [];
588
+ for (const cand of data.candidates ?? []) {
589
+ for (const part of cand.content?.parts ?? []) {
590
+ if (part.text) {
591
+ textBlocks.push({ type: 'text', text: part.text });
592
+ }
593
+ if (part.functionCall) {
594
+ toolBlocks.push({
595
+ type: 'tool_use',
596
+ id: this.genUUID(),
597
+ name: part.functionCall.name,
598
+ input: part.functionCall.args ?? {},
599
+ });
600
+ }
601
+ if (part.functionResponse) {
602
+ toolBlocks.push({
603
+ type: 'tool_result',
604
+ tool_use_id: part.functionResponse.name,
605
+ content: JSON.stringify(part.functionResponse.response),
606
+ });
607
+ }
608
+ }
609
+ }
610
+ const blocks = [...textBlocks, ...toolBlocks];
611
+ return {
612
+ blocks,
613
+ stop_reason: toolBlocks.some((b) => b.type === 'tool_use')
614
+ ? 'tool_use'
615
+ : 'end',
616
+ };
617
+ }
618
+ /* ────────────────────────────────────────────────────────── */
619
+ /* chatOnce (text) */
620
+ /* ────────────────────────────────────────────────────────── */
621
+ async chatOnce(messages, stream = true, onPartialResponse = () => { }, maxTokens) {
622
+ const res = await this.callGemini(messages, this.model, stream, maxTokens);
623
+ return stream
624
+ ? this.parseStream(res, onPartialResponse)
625
+ : this.parseOneShot(await res.json());
626
+ }
627
+ /* ────────────────────────────────────────────────────────── */
628
+ /* visionChatOnce (images) */
629
+ /* ────────────────────────────────────────────────────────── */
630
+ async visionChatOnce(messages, stream = false, onPartialResponse = () => { }, maxTokens) {
631
+ const res = await this.callGemini(messages, this.visionModel, stream, maxTokens);
632
+ return stream
633
+ ? this.parseStream(res, onPartialResponse)
634
+ : this.parseOneShot(await res.json());
635
+ }
636
+ /* ────────────────────────────────────────────────────────── */
637
+ /* UUID helper */
638
+ /* ────────────────────────────────────────────────────────── */
639
+ genUUID() {
640
+ return typeof crypto !== 'undefined' && crypto.randomUUID
641
+ ? crypto.randomUUID()
642
+ : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
643
+ const r = (Math.random() * 16) | 0;
644
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
645
+ return v.toString(16);
646
+ });
647
+ }
648
+ }
649
+ //# sourceMappingURL=GeminiChatService.js.map