@aituber-onair/chat 0.35.0 → 0.37.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 (91) hide show
  1. package/README.ja.md +6 -6
  2. package/README.md +7 -8
  3. package/dist/cjs/constants/claude.d.ts +1 -0
  4. package/dist/cjs/constants/claude.d.ts.map +1 -1
  5. package/dist/cjs/constants/claude.js +3 -1
  6. package/dist/cjs/constants/claude.js.map +1 -1
  7. package/dist/cjs/constants/gemini.d.ts +0 -7
  8. package/dist/cjs/constants/gemini.d.ts.map +1 -1
  9. package/dist/cjs/constants/gemini.js +1 -10
  10. package/dist/cjs/constants/gemini.js.map +1 -1
  11. package/dist/cjs/services/providers/claude/ClaudeChatService.d.ts +0 -24
  12. package/dist/cjs/services/providers/claude/ClaudeChatService.d.ts.map +1 -1
  13. package/dist/cjs/services/providers/claude/ClaudeChatService.js +3 -113
  14. package/dist/cjs/services/providers/claude/ClaudeChatService.js.map +1 -1
  15. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.d.ts.map +1 -1
  16. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.js +1 -0
  17. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.js.map +1 -1
  18. package/dist/cjs/services/providers/claude/claudeMessageConverter.d.ts +6 -0
  19. package/dist/cjs/services/providers/claude/claudeMessageConverter.d.ts.map +1 -0
  20. package/dist/cjs/services/providers/claude/claudeMessageConverter.js +95 -0
  21. package/dist/cjs/services/providers/claude/claudeMessageConverter.js.map +1 -0
  22. package/dist/cjs/services/providers/gemini/GeminiChatService.d.ts +0 -21
  23. package/dist/cjs/services/providers/gemini/GeminiChatService.d.ts.map +1 -1
  24. package/dist/cjs/services/providers/gemini/GeminiChatService.js +14 -254
  25. package/dist/cjs/services/providers/gemini/GeminiChatService.js.map +1 -1
  26. package/dist/cjs/services/providers/gemini/geminiMessageConverter.d.ts +17 -0
  27. package/dist/cjs/services/providers/gemini/geminiMessageConverter.d.ts.map +1 -0
  28. package/dist/cjs/services/providers/gemini/geminiMessageConverter.js +183 -0
  29. package/dist/cjs/services/providers/gemini/geminiMessageConverter.js.map +1 -0
  30. package/dist/cjs/services/providers/gemini/geminiToolAdapter.d.ts +23 -0
  31. package/dist/cjs/services/providers/gemini/geminiToolAdapter.d.ts.map +1 -0
  32. package/dist/cjs/services/providers/gemini/geminiToolAdapter.js +47 -0
  33. package/dist/cjs/services/providers/gemini/geminiToolAdapter.js.map +1 -0
  34. package/dist/cjs/services/providers/openai/OpenAIChatService.d.ts +3 -28
  35. package/dist/cjs/services/providers/openai/OpenAIChatService.d.ts.map +1 -1
  36. package/dist/cjs/services/providers/openai/OpenAIChatService.js +15 -250
  37. package/dist/cjs/services/providers/openai/OpenAIChatService.js.map +1 -1
  38. package/dist/cjs/services/providers/openai/openaiRequestBuilder.d.ts +33 -0
  39. package/dist/cjs/services/providers/openai/openaiRequestBuilder.d.ts.map +1 -0
  40. package/dist/cjs/services/providers/openai/openaiRequestBuilder.js +218 -0
  41. package/dist/cjs/services/providers/openai/openaiRequestBuilder.js.map +1 -0
  42. package/dist/cjs/services/providers/openai/openaiToolBuilder.d.ts +9 -0
  43. package/dist/cjs/services/providers/openai/openaiToolBuilder.d.ts.map +1 -0
  44. package/dist/cjs/services/providers/openai/openaiToolBuilder.js +36 -0
  45. package/dist/cjs/services/providers/openai/openaiToolBuilder.js.map +1 -0
  46. package/dist/esm/constants/claude.d.ts +1 -0
  47. package/dist/esm/constants/claude.d.ts.map +1 -1
  48. package/dist/esm/constants/claude.js +2 -0
  49. package/dist/esm/constants/claude.js.map +1 -1
  50. package/dist/esm/constants/gemini.d.ts +0 -7
  51. package/dist/esm/constants/gemini.d.ts.map +1 -1
  52. package/dist/esm/constants/gemini.js +0 -9
  53. package/dist/esm/constants/gemini.js.map +1 -1
  54. package/dist/esm/services/providers/claude/ClaudeChatService.d.ts +0 -24
  55. package/dist/esm/services/providers/claude/ClaudeChatService.d.ts.map +1 -1
  56. package/dist/esm/services/providers/claude/ClaudeChatService.js +3 -113
  57. package/dist/esm/services/providers/claude/ClaudeChatService.js.map +1 -1
  58. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.d.ts.map +1 -1
  59. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.js +2 -1
  60. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.js.map +1 -1
  61. package/dist/esm/services/providers/claude/claudeMessageConverter.d.ts +6 -0
  62. package/dist/esm/services/providers/claude/claudeMessageConverter.d.ts.map +1 -0
  63. package/dist/esm/services/providers/claude/claudeMessageConverter.js +89 -0
  64. package/dist/esm/services/providers/claude/claudeMessageConverter.js.map +1 -0
  65. package/dist/esm/services/providers/gemini/GeminiChatService.d.ts +0 -21
  66. package/dist/esm/services/providers/gemini/GeminiChatService.d.ts.map +1 -1
  67. package/dist/esm/services/providers/gemini/GeminiChatService.js +14 -254
  68. package/dist/esm/services/providers/gemini/GeminiChatService.js.map +1 -1
  69. package/dist/esm/services/providers/gemini/geminiMessageConverter.d.ts +17 -0
  70. package/dist/esm/services/providers/gemini/geminiMessageConverter.d.ts.map +1 -0
  71. package/dist/esm/services/providers/gemini/geminiMessageConverter.js +178 -0
  72. package/dist/esm/services/providers/gemini/geminiMessageConverter.js.map +1 -0
  73. package/dist/esm/services/providers/gemini/geminiToolAdapter.d.ts +23 -0
  74. package/dist/esm/services/providers/gemini/geminiToolAdapter.d.ts.map +1 -0
  75. package/dist/esm/services/providers/gemini/geminiToolAdapter.js +42 -0
  76. package/dist/esm/services/providers/gemini/geminiToolAdapter.js.map +1 -0
  77. package/dist/esm/services/providers/openai/OpenAIChatService.d.ts +3 -28
  78. package/dist/esm/services/providers/openai/OpenAIChatService.d.ts.map +1 -1
  79. package/dist/esm/services/providers/openai/OpenAIChatService.js +17 -252
  80. package/dist/esm/services/providers/openai/OpenAIChatService.js.map +1 -1
  81. package/dist/esm/services/providers/openai/openaiRequestBuilder.d.ts +33 -0
  82. package/dist/esm/services/providers/openai/openaiRequestBuilder.d.ts.map +1 -0
  83. package/dist/esm/services/providers/openai/openaiRequestBuilder.js +212 -0
  84. package/dist/esm/services/providers/openai/openaiRequestBuilder.js.map +1 -0
  85. package/dist/esm/services/providers/openai/openaiToolBuilder.d.ts +9 -0
  86. package/dist/esm/services/providers/openai/openaiToolBuilder.d.ts.map +1 -0
  87. package/dist/esm/services/providers/openai/openaiToolBuilder.js +33 -0
  88. package/dist/esm/services/providers/openai/openaiToolBuilder.js.map +1 -0
  89. package/dist/umd/aituber-onair-chat.js +552 -548
  90. package/dist/umd/aituber-onair-chat.min.js +10 -10
  91. package/package.json +1 -1
@@ -84,14 +84,13 @@ var AITuberOnAirChat = (() => {
84
84
  MODEL_CLAUDE_4_6_OPUS: () => MODEL_CLAUDE_4_6_OPUS,
85
85
  MODEL_CLAUDE_4_6_SONNET: () => MODEL_CLAUDE_4_6_SONNET,
86
86
  MODEL_CLAUDE_4_7_OPUS: () => MODEL_CLAUDE_4_7_OPUS,
87
+ MODEL_CLAUDE_4_8_OPUS: () => MODEL_CLAUDE_4_8_OPUS,
87
88
  MODEL_CLAUDE_4_OPUS: () => MODEL_CLAUDE_4_OPUS,
88
89
  MODEL_CLAUDE_4_SONNET: () => MODEL_CLAUDE_4_SONNET,
89
90
  MODEL_DEEPSEEK_CHAT: () => MODEL_DEEPSEEK_CHAT,
90
91
  MODEL_DEEPSEEK_REASONER: () => MODEL_DEEPSEEK_REASONER,
91
92
  MODEL_DEEPSEEK_V4_FLASH: () => MODEL_DEEPSEEK_V4_FLASH,
92
93
  MODEL_DEEPSEEK_V4_PRO: () => MODEL_DEEPSEEK_V4_PRO,
93
- MODEL_GEMINI_2_0_FLASH: () => MODEL_GEMINI_2_0_FLASH,
94
- MODEL_GEMINI_2_0_FLASH_LITE: () => MODEL_GEMINI_2_0_FLASH_LITE,
95
94
  MODEL_GEMINI_2_5_FLASH: () => MODEL_GEMINI_2_5_FLASH,
96
95
  MODEL_GEMINI_2_5_FLASH_LITE: () => MODEL_GEMINI_2_5_FLASH_LITE,
97
96
  MODEL_GEMINI_2_5_FLASH_LITE_PREVIEW_06_17: () => MODEL_GEMINI_2_5_FLASH_LITE_PREVIEW_06_17,
@@ -307,8 +306,6 @@ var AITuberOnAirChat = (() => {
307
306
  var MODEL_GEMINI_2_5_FLASH = "gemini-2.5-flash";
308
307
  var MODEL_GEMINI_2_5_FLASH_LITE = "gemini-2.5-flash-lite";
309
308
  var MODEL_GEMINI_2_5_FLASH_LITE_PREVIEW_06_17 = "gemini-2.5-flash-lite-preview-06-17";
310
- var MODEL_GEMINI_2_0_FLASH = "gemini-2.0-flash";
311
- var MODEL_GEMINI_2_0_FLASH_LITE = "gemini-2.0-flash-lite";
312
309
  var GEMINI_RECOMMENDED_MODELS = [
313
310
  MODEL_GEMINI_3_5_FLASH,
314
311
  MODEL_GEMINI_3_1_FLASH_LITE,
@@ -323,9 +320,7 @@ var AITuberOnAirChat = (() => {
323
320
  var GEMINI_DEPRECATED_MODELS = [
324
321
  MODEL_GEMINI_3_1_FLASH_LITE_PREVIEW,
325
322
  MODEL_GEMINI_3_PRO_PREVIEW,
326
- MODEL_GEMINI_2_5_FLASH_LITE_PREVIEW_06_17,
327
- MODEL_GEMINI_2_0_FLASH,
328
- MODEL_GEMINI_2_0_FLASH_LITE
323
+ MODEL_GEMINI_2_5_FLASH_LITE_PREVIEW_06_17
329
324
  ];
330
325
  var GEMINI_VISION_SUPPORTED_MODELS = [
331
326
  ...GEMINI_RECOMMENDED_MODELS,
@@ -346,6 +341,7 @@ var AITuberOnAirChat = (() => {
346
341
  var MODEL_CLAUDE_4_6_SONNET = "claude-sonnet-4-6";
347
342
  var MODEL_CLAUDE_4_6_OPUS = "claude-opus-4-6";
348
343
  var MODEL_CLAUDE_4_7_OPUS = "claude-opus-4-7";
344
+ var MODEL_CLAUDE_4_8_OPUS = "claude-opus-4-8";
349
345
  var CLAUDE_VISION_SUPPORTED_MODELS = [
350
346
  MODEL_CLAUDE_3_HAIKU,
351
347
  MODEL_CLAUDE_4_SONNET,
@@ -355,7 +351,8 @@ var AITuberOnAirChat = (() => {
355
351
  MODEL_CLAUDE_4_5_OPUS,
356
352
  MODEL_CLAUDE_4_6_SONNET,
357
353
  MODEL_CLAUDE_4_6_OPUS,
358
- MODEL_CLAUDE_4_7_OPUS
354
+ MODEL_CLAUDE_4_7_OPUS,
355
+ MODEL_CLAUDE_4_8_OPUS
359
356
  ];
360
357
 
361
358
  // src/constants/openrouter.ts
@@ -1247,6 +1244,94 @@ If it's in another language, summarize in that language.
1247
1244
  };
1248
1245
  }
1249
1246
 
1247
+ // src/services/providers/claude/claudeMessageConverter.ts
1248
+ function convertMessagesToClaudeFormat(messages) {
1249
+ return messages.map((msg) => ({
1250
+ role: mapRoleToClaude(msg.role),
1251
+ content: msg.content
1252
+ }));
1253
+ }
1254
+ function convertVisionMessagesToClaudeFormat(messages) {
1255
+ return messages.map((msg) => {
1256
+ if (typeof msg.content === "string") {
1257
+ return {
1258
+ role: mapRoleToClaude(msg.role),
1259
+ content: [
1260
+ {
1261
+ type: "text",
1262
+ text: msg.content
1263
+ }
1264
+ ]
1265
+ };
1266
+ }
1267
+ if (Array.isArray(msg.content)) {
1268
+ const content = msg.content.map((block) => {
1269
+ if (block.type === "image_url") {
1270
+ if (block.image_url.url.startsWith("data:")) {
1271
+ const m = block.image_url.url.match(/^data:([^;]+);base64,(.+)$/);
1272
+ if (m) {
1273
+ return {
1274
+ type: "image",
1275
+ source: {
1276
+ type: "base64",
1277
+ media_type: m[1],
1278
+ data: m[2]
1279
+ }
1280
+ };
1281
+ }
1282
+ return null;
1283
+ }
1284
+ return {
1285
+ type: "image",
1286
+ source: {
1287
+ type: "url",
1288
+ url: block.image_url.url,
1289
+ media_type: getMimeTypeFromUrl(block.image_url.url)
1290
+ }
1291
+ };
1292
+ }
1293
+ return block;
1294
+ }).filter((b) => b);
1295
+ return {
1296
+ role: mapRoleToClaude(msg.role),
1297
+ content
1298
+ };
1299
+ }
1300
+ return {
1301
+ role: mapRoleToClaude(msg.role),
1302
+ content: []
1303
+ };
1304
+ });
1305
+ }
1306
+ function mapRoleToClaude(role) {
1307
+ switch (role) {
1308
+ case "system":
1309
+ return "system";
1310
+ case "user":
1311
+ return "user";
1312
+ case "assistant":
1313
+ return "assistant";
1314
+ default:
1315
+ return "user";
1316
+ }
1317
+ }
1318
+ function getMimeTypeFromUrl(url) {
1319
+ const extension = url.split(".").pop()?.toLowerCase();
1320
+ switch (extension) {
1321
+ case "jpg":
1322
+ case "jpeg":
1323
+ return "image/jpeg";
1324
+ case "png":
1325
+ return "image/png";
1326
+ case "gif":
1327
+ return "image/gif";
1328
+ case "webp":
1329
+ return "image/webp";
1330
+ default:
1331
+ return "image/jpeg";
1332
+ }
1333
+ }
1334
+
1250
1335
  // src/services/providers/claude/ClaudeChatService.ts
1251
1336
  var ClaudeChatService = class {
1252
1337
  /**
@@ -1353,112 +1438,6 @@ If it's in another language, summarize in that language.
1353
1438
  toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
1354
1439
  });
1355
1440
  }
1356
- /**
1357
- * Convert AITuber OnAir messages to Claude format
1358
- * @param messages Array of messages
1359
- * @returns Claude formatted messages
1360
- */
1361
- convertMessagesToClaudeFormat(messages) {
1362
- return messages.map((msg) => {
1363
- return {
1364
- role: this.mapRoleToClaude(msg.role),
1365
- content: msg.content
1366
- };
1367
- });
1368
- }
1369
- /**
1370
- * Convert AITuber OnAir vision messages to Claude format
1371
- * @param messages Array of vision messages
1372
- * @returns Claude formatted vision messages
1373
- */
1374
- convertVisionMessagesToClaudeFormat(messages) {
1375
- return messages.map((msg) => {
1376
- if (typeof msg.content === "string") {
1377
- return {
1378
- role: this.mapRoleToClaude(msg.role),
1379
- content: [
1380
- {
1381
- type: "text",
1382
- text: msg.content
1383
- }
1384
- ]
1385
- };
1386
- }
1387
- if (Array.isArray(msg.content)) {
1388
- const content = msg.content.map((block) => {
1389
- if (block.type === "image_url") {
1390
- if (block.image_url.url.startsWith("data:")) {
1391
- const m = block.image_url.url.match(
1392
- /^data:([^;]+);base64,(.+)$/
1393
- );
1394
- if (m) {
1395
- return {
1396
- type: "image",
1397
- source: { type: "base64", media_type: m[1], data: m[2] }
1398
- };
1399
- }
1400
- return null;
1401
- }
1402
- return {
1403
- type: "image",
1404
- source: {
1405
- type: "url",
1406
- url: block.image_url.url,
1407
- media_type: this.getMimeTypeFromUrl(block.image_url.url)
1408
- }
1409
- };
1410
- }
1411
- return block;
1412
- }).filter((b) => b);
1413
- return {
1414
- role: this.mapRoleToClaude(msg.role),
1415
- content
1416
- };
1417
- }
1418
- return {
1419
- role: this.mapRoleToClaude(msg.role),
1420
- content: []
1421
- };
1422
- });
1423
- }
1424
- /**
1425
- * Map AITuber OnAir roles to Claude roles
1426
- * @param role AITuber OnAir role
1427
- * @returns Claude role
1428
- */
1429
- mapRoleToClaude(role) {
1430
- switch (role) {
1431
- case "system":
1432
- return "system";
1433
- case "user":
1434
- return "user";
1435
- case "assistant":
1436
- return "assistant";
1437
- default:
1438
- return "user";
1439
- }
1440
- }
1441
- /**
1442
- * Get MIME type from URL
1443
- * @param url Image URL
1444
- * @returns MIME type
1445
- */
1446
- getMimeTypeFromUrl(url) {
1447
- const extension = url.split(".").pop()?.toLowerCase();
1448
- switch (extension) {
1449
- case "jpg":
1450
- case "jpeg":
1451
- return "image/jpeg";
1452
- case "png":
1453
- return "image/png";
1454
- case "gif":
1455
- return "image/gif";
1456
- case "webp":
1457
- return "image/webp";
1458
- default:
1459
- return "image/jpeg";
1460
- }
1461
- }
1462
1441
  /**
1463
1442
  * Call Claude API
1464
1443
  * @param messages Array of messages to send
@@ -1478,9 +1457,7 @@ If it's in another language, summarize in that language.
1478
1457
  const body = {
1479
1458
  model,
1480
1459
  system,
1481
- messages: hasVision ? this.convertVisionMessagesToClaudeFormat(
1482
- content
1483
- ) : this.convertMessagesToClaudeFormat(content),
1460
+ messages: hasVision ? convertVisionMessagesToClaudeFormat(content) : convertMessagesToClaudeFormat(content),
1484
1461
  stream,
1485
1462
  max_tokens: maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength)
1486
1463
  };
@@ -1743,6 +1720,7 @@ If it's in another language, summarize in that language.
1743
1720
  MODEL_CLAUDE_4_6_SONNET,
1744
1721
  MODEL_CLAUDE_4_6_OPUS,
1745
1722
  MODEL_CLAUDE_4_7_OPUS,
1723
+ MODEL_CLAUDE_4_8_OPUS,
1746
1724
  MODEL_CLAUDE_3_HAIKU
1747
1725
  ];
1748
1726
  }
@@ -1934,7 +1912,49 @@ If it's in another language, summarize in that language.
1934
1912
  };
1935
1913
  }
1936
1914
 
1937
- // src/services/providers/openai/OpenAIChatService.ts
1915
+ // src/services/providers/openai/openaiToolBuilder.ts
1916
+ function buildOpenAIToolsDefinition({
1917
+ tools,
1918
+ mcpServers,
1919
+ isResponsesAPI
1920
+ }) {
1921
+ const toolDefs = [];
1922
+ if (tools.length > 0) {
1923
+ toolDefs.push(
1924
+ ...buildOpenAICompatibleTools(
1925
+ tools,
1926
+ isResponsesAPI ? "responses" : "chat-completions"
1927
+ )
1928
+ );
1929
+ }
1930
+ if (mcpServers.length > 0 && isResponsesAPI) {
1931
+ toolDefs.push(...buildOpenAIMCPToolsDefinition(mcpServers));
1932
+ }
1933
+ return toolDefs;
1934
+ }
1935
+ function buildOpenAIMCPToolsDefinition(mcpServers) {
1936
+ return mcpServers.map((server) => {
1937
+ const mcpDef = {
1938
+ type: "mcp",
1939
+ server_label: server.name,
1940
+ server_url: server.url
1941
+ };
1942
+ if (server.require_approval) {
1943
+ mcpDef.require_approval = server.require_approval;
1944
+ }
1945
+ if (server.tool_configuration?.allowed_tools) {
1946
+ mcpDef.allowed_tools = server.tool_configuration.allowed_tools;
1947
+ }
1948
+ if (server.authorization_token) {
1949
+ mcpDef.headers = {
1950
+ Authorization: `Bearer ${server.authorization_token}`
1951
+ };
1952
+ }
1953
+ return mcpDef;
1954
+ });
1955
+ }
1956
+
1957
+ // src/services/providers/openai/openaiRequestBuilder.ts
1938
1958
  var GPT5_RESPONSE_LENGTH_MIN_TOKENS = {
1939
1959
  [CHAT_RESPONSE_LENGTH.VERY_SHORT]: 800,
1940
1960
  [CHAT_RESPONSE_LENGTH.SHORT]: 1200,
@@ -1956,6 +1976,177 @@ If it's in another language, summarize in that language.
1956
1976
  "deepseek",
1957
1977
  "mistral"
1958
1978
  ]);
1979
+ function buildOpenAIRequestBody({
1980
+ provider,
1981
+ endpoint,
1982
+ messages,
1983
+ model,
1984
+ stream,
1985
+ tools,
1986
+ mcpServers,
1987
+ responseLength,
1988
+ verbosity,
1989
+ reasoning_effort,
1990
+ enableReasoningSummary,
1991
+ maxTokens
1992
+ }) {
1993
+ const isResponsesAPI = endpoint === ENDPOINT_OPENAI_RESPONSES_API;
1994
+ validateMCPCompatibility(endpoint, mcpServers);
1995
+ const body = {
1996
+ model,
1997
+ stream
1998
+ };
1999
+ const tokenLimit = resolveOpenAITokenLimit({
2000
+ provider,
2001
+ model,
2002
+ responseLength,
2003
+ reasoning_effort,
2004
+ maxTokens
2005
+ });
2006
+ if (isResponsesAPI) {
2007
+ if (tokenLimit !== void 0) {
2008
+ body.max_output_tokens = tokenLimit;
2009
+ }
2010
+ } else {
2011
+ if (tokenLimit !== void 0) {
2012
+ if (usesCompatibleChatCompletions(provider)) {
2013
+ body.max_tokens = tokenLimit;
2014
+ } else {
2015
+ body.max_completion_tokens = tokenLimit;
2016
+ }
2017
+ }
2018
+ }
2019
+ if (isResponsesAPI) {
2020
+ body.input = cleanMessagesForResponsesAPI(messages);
2021
+ } else {
2022
+ body.messages = provider === "mistral" ? cleanMessagesForMistralChatCompletions(messages) : messages;
2023
+ }
2024
+ if (isGPT5Model(model)) {
2025
+ if (isResponsesAPI) {
2026
+ if (reasoning_effort) {
2027
+ body.reasoning = {
2028
+ ...body.reasoning,
2029
+ effort: reasoning_effort
2030
+ };
2031
+ if (enableReasoningSummary) {
2032
+ body.reasoning.summary = "auto";
2033
+ }
2034
+ }
2035
+ if (verbosity) {
2036
+ body.text = {
2037
+ ...body.text,
2038
+ format: { type: "text" },
2039
+ verbosity
2040
+ };
2041
+ }
2042
+ } else {
2043
+ if (reasoning_effort) {
2044
+ body.reasoning_effort = reasoning_effort;
2045
+ }
2046
+ if (verbosity) {
2047
+ body.verbosity = verbosity;
2048
+ }
2049
+ }
2050
+ }
2051
+ if (provider === "mistral" && isMistralReasoningEffortModel(model) && reasoning_effort && isMistralReasoningEffort(reasoning_effort)) {
2052
+ body.reasoning_effort = reasoning_effort;
2053
+ }
2054
+ const toolDefinitions = buildOpenAIToolsDefinition({
2055
+ tools,
2056
+ mcpServers,
2057
+ isResponsesAPI
2058
+ });
2059
+ if (toolDefinitions.length > 0) {
2060
+ body.tools = toolDefinitions;
2061
+ if (!isResponsesAPI) {
2062
+ body.tool_choice = "auto";
2063
+ }
2064
+ }
2065
+ return body;
2066
+ }
2067
+ function resolveOpenAITokenLimit({
2068
+ provider,
2069
+ model,
2070
+ responseLength,
2071
+ reasoning_effort,
2072
+ maxTokens
2073
+ }) {
2074
+ if (maxTokens !== void 0) {
2075
+ return maxTokens;
2076
+ }
2077
+ const baseTokenLimit = usesCompatibleChatCompletions(provider) ? responseLength !== void 0 ? getMaxTokensForResponseLength(responseLength) : void 0 : getMaxTokensForResponseLength(responseLength);
2078
+ if (provider !== "openai" || !isGPT5Model(model) || responseLength === void 0) {
2079
+ return baseTokenLimit;
2080
+ }
2081
+ const effectiveReasoningEffort = reasoning_effort ?? getDefaultReasoningEffortForGPT5Model(model);
2082
+ return Math.max(
2083
+ baseTokenLimit ?? 0,
2084
+ GPT5_RESPONSE_LENGTH_MIN_TOKENS[responseLength],
2085
+ GPT5_REASONING_MIN_TOKENS[effectiveReasoningEffort]
2086
+ );
2087
+ }
2088
+ function validateMCPCompatibility(endpoint, mcpServers) {
2089
+ if (mcpServers.length > 0 && endpoint === ENDPOINT_OPENAI_CHAT_COMPLETIONS_API) {
2090
+ throw new Error(
2091
+ `MCP servers are not supported with Chat Completions API. Current endpoint: ${endpoint}. Please use OpenAI Responses API endpoint: ${ENDPOINT_OPENAI_RESPONSES_API}. MCP tools are only available in the Responses API endpoint.`
2092
+ );
2093
+ }
2094
+ }
2095
+ function cleanMessagesForResponsesAPI(messages) {
2096
+ return messages.map((msg) => {
2097
+ const role = msg.role === "tool" ? "user" : msg.role;
2098
+ const cleanMsg = {
2099
+ role
2100
+ };
2101
+ if (typeof msg.content === "string") {
2102
+ cleanMsg.content = msg.content;
2103
+ } else if (Array.isArray(msg.content)) {
2104
+ cleanMsg.content = msg.content.map((block) => {
2105
+ if (block.type === "text") {
2106
+ return {
2107
+ type: "input_text",
2108
+ text: block.text
2109
+ };
2110
+ } else if (block.type === "image_url") {
2111
+ return {
2112
+ type: "input_image",
2113
+ image_url: block.image_url.url
2114
+ };
2115
+ }
2116
+ return block;
2117
+ });
2118
+ } else {
2119
+ cleanMsg.content = msg.content;
2120
+ }
2121
+ return cleanMsg;
2122
+ });
2123
+ }
2124
+ function cleanMessagesForMistralChatCompletions(messages) {
2125
+ return messages.map((msg) => {
2126
+ const cleanMsg = {
2127
+ role: msg.role
2128
+ };
2129
+ if (!Array.isArray(msg.content)) {
2130
+ cleanMsg.content = msg.content;
2131
+ return cleanMsg;
2132
+ }
2133
+ cleanMsg.content = msg.content.map((block) => {
2134
+ if (block.type === "image_url" && typeof block.image_url === "object" && typeof block.image_url?.url === "string") {
2135
+ return {
2136
+ type: "image_url",
2137
+ image_url: block.image_url.url
2138
+ };
2139
+ }
2140
+ return block;
2141
+ });
2142
+ return cleanMsg;
2143
+ });
2144
+ }
2145
+ function usesCompatibleChatCompletions(provider) {
2146
+ return OPENAI_COMPATIBLE_CHAT_COMPLETIONS_PROVIDERS.has(provider);
2147
+ }
2148
+
2149
+ // src/services/providers/openai/OpenAIChatService.ts
1959
2150
  var OpenAIChatService = class {
1960
2151
  /**
1961
2152
  * Constructor
@@ -2108,7 +2299,20 @@ If it's in another language, summarize in that language.
2108
2299
  return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
2109
2300
  }
2110
2301
  async callOpenAI(messages, model, stream = false, maxTokens) {
2111
- const body = this.buildRequestBody(messages, model, stream, maxTokens);
2302
+ const body = buildOpenAIRequestBody({
2303
+ provider: this.provider,
2304
+ endpoint: this.endpoint,
2305
+ messages,
2306
+ model,
2307
+ stream,
2308
+ tools: this.tools,
2309
+ mcpServers: this.mcpServers,
2310
+ responseLength: this.responseLength,
2311
+ verbosity: this.verbosity,
2312
+ reasoning_effort: this.reasoning_effort,
2313
+ enableReasoningSummary: this.enableReasoningSummary,
2314
+ maxTokens
2315
+ });
2112
2316
  const headers = {};
2113
2317
  const shouldSendAuthorization = this.provider !== "openai-compatible" || this.apiKey.trim() !== "";
2114
2318
  if (shouldSendAuthorization) {
@@ -2117,199 +2321,6 @@ If it's in another language, summarize in that language.
2117
2321
  const res = await ChatServiceHttpClient.post(this.endpoint, body, headers);
2118
2322
  return res;
2119
2323
  }
2120
- /**
2121
- * Build request body based on the endpoint type
2122
- */
2123
- buildRequestBody(messages, model, stream, maxTokens) {
2124
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2125
- this.validateMCPCompatibility();
2126
- const body = {
2127
- model,
2128
- stream
2129
- };
2130
- const tokenLimit = this.resolveTokenLimit(model, maxTokens);
2131
- if (isResponsesAPI) {
2132
- if (tokenLimit !== void 0) {
2133
- body.max_output_tokens = tokenLimit;
2134
- }
2135
- } else {
2136
- if (tokenLimit !== void 0) {
2137
- if (this.usesCompatibleChatCompletions()) {
2138
- body.max_tokens = tokenLimit;
2139
- } else {
2140
- body.max_completion_tokens = tokenLimit;
2141
- }
2142
- }
2143
- }
2144
- if (isResponsesAPI) {
2145
- body.input = this.cleanMessagesForResponsesAPI(messages);
2146
- } else {
2147
- body.messages = this.provider === "mistral" ? this.cleanMessagesForMistralChatCompletions(messages) : messages;
2148
- }
2149
- if (isGPT5Model(model)) {
2150
- if (isResponsesAPI) {
2151
- if (this.reasoning_effort) {
2152
- body.reasoning = {
2153
- ...body.reasoning,
2154
- effort: this.reasoning_effort
2155
- };
2156
- if (this.enableReasoningSummary) {
2157
- body.reasoning.summary = "auto";
2158
- }
2159
- }
2160
- if (this.verbosity) {
2161
- body.text = {
2162
- ...body.text,
2163
- format: { type: "text" },
2164
- verbosity: this.verbosity
2165
- };
2166
- }
2167
- } else {
2168
- if (this.reasoning_effort) {
2169
- body.reasoning_effort = this.reasoning_effort;
2170
- }
2171
- if (this.verbosity) {
2172
- body.verbosity = this.verbosity;
2173
- }
2174
- }
2175
- }
2176
- if (this.provider === "mistral" && isMistralReasoningEffortModel(model) && this.reasoning_effort && isMistralReasoningEffort(this.reasoning_effort)) {
2177
- body.reasoning_effort = this.reasoning_effort;
2178
- }
2179
- const tools = this.buildToolsDefinition();
2180
- if (tools.length > 0) {
2181
- body.tools = tools;
2182
- if (!isResponsesAPI) {
2183
- body.tool_choice = "auto";
2184
- }
2185
- }
2186
- return body;
2187
- }
2188
- resolveTokenLimit(model, maxTokens) {
2189
- if (maxTokens !== void 0) {
2190
- return maxTokens;
2191
- }
2192
- const baseTokenLimit = this.usesCompatibleChatCompletions() ? this.responseLength !== void 0 ? getMaxTokensForResponseLength(this.responseLength) : void 0 : getMaxTokensForResponseLength(this.responseLength);
2193
- if (this.provider !== "openai" || !isGPT5Model(model) || this.responseLength === void 0) {
2194
- return baseTokenLimit;
2195
- }
2196
- const effectiveReasoningEffort = this.reasoning_effort ?? getDefaultReasoningEffortForGPT5Model(model);
2197
- return Math.max(
2198
- baseTokenLimit ?? 0,
2199
- GPT5_RESPONSE_LENGTH_MIN_TOKENS[this.responseLength],
2200
- GPT5_REASONING_MIN_TOKENS[effectiveReasoningEffort]
2201
- );
2202
- }
2203
- /**
2204
- * Validate MCP servers compatibility with the current endpoint
2205
- */
2206
- validateMCPCompatibility() {
2207
- if (this.mcpServers.length > 0 && this.endpoint === ENDPOINT_OPENAI_CHAT_COMPLETIONS_API) {
2208
- throw new Error(
2209
- `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.`
2210
- );
2211
- }
2212
- }
2213
- /**
2214
- * Clean messages for Responses API (remove timestamp and other extra properties)
2215
- */
2216
- cleanMessagesForResponsesAPI(messages) {
2217
- return messages.map((msg) => {
2218
- const role = msg.role === "tool" ? "user" : msg.role;
2219
- const cleanMsg = {
2220
- role
2221
- };
2222
- if (typeof msg.content === "string") {
2223
- cleanMsg.content = msg.content;
2224
- } else if (Array.isArray(msg.content)) {
2225
- cleanMsg.content = msg.content.map((block) => {
2226
- if (block.type === "text") {
2227
- return {
2228
- type: "input_text",
2229
- text: block.text
2230
- };
2231
- } else if (block.type === "image_url") {
2232
- return {
2233
- type: "input_image",
2234
- image_url: block.image_url.url
2235
- // Extract the URL string directly
2236
- };
2237
- }
2238
- return block;
2239
- });
2240
- } else {
2241
- cleanMsg.content = msg.content;
2242
- }
2243
- return cleanMsg;
2244
- });
2245
- }
2246
- cleanMessagesForMistralChatCompletions(messages) {
2247
- return messages.map((msg) => {
2248
- const cleanMsg = {
2249
- role: msg.role
2250
- };
2251
- if (!Array.isArray(msg.content)) {
2252
- cleanMsg.content = msg.content;
2253
- return cleanMsg;
2254
- }
2255
- cleanMsg.content = msg.content.map((block) => {
2256
- if (block.type === "image_url" && typeof block.image_url === "object" && typeof block.image_url?.url === "string") {
2257
- return {
2258
- type: "image_url",
2259
- image_url: block.image_url.url
2260
- };
2261
- }
2262
- return block;
2263
- });
2264
- return cleanMsg;
2265
- });
2266
- }
2267
- /**
2268
- * Build tools definition based on the endpoint type
2269
- */
2270
- buildToolsDefinition() {
2271
- const isResponsesAPI = this.endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2272
- const toolDefs = [];
2273
- if (this.tools.length > 0) {
2274
- toolDefs.push(
2275
- ...buildOpenAICompatibleTools(
2276
- this.tools,
2277
- isResponsesAPI ? "responses" : "chat-completions"
2278
- )
2279
- );
2280
- }
2281
- if (this.mcpServers.length > 0 && isResponsesAPI) {
2282
- toolDefs.push(...this.buildMCPToolsDefinition());
2283
- }
2284
- return toolDefs;
2285
- }
2286
- /**
2287
- * Build MCP tools definition for Responses API
2288
- */
2289
- buildMCPToolsDefinition() {
2290
- return this.mcpServers.map((server) => {
2291
- const mcpDef = {
2292
- type: "mcp",
2293
- // Using 'mcp' as indicated by the error message
2294
- server_label: server.name,
2295
- // Use server_label as required by API
2296
- server_url: server.url
2297
- // Use server_url instead of url
2298
- };
2299
- if (server.require_approval) {
2300
- mcpDef.require_approval = server.require_approval;
2301
- }
2302
- if (server.tool_configuration?.allowed_tools) {
2303
- mcpDef.allowed_tools = server.tool_configuration.allowed_tools;
2304
- }
2305
- if (server.authorization_token) {
2306
- mcpDef.headers = {
2307
- Authorization: `Bearer ${server.authorization_token}`
2308
- };
2309
- }
2310
- return mcpDef;
2311
- });
2312
- }
2313
2324
  async handleStream(res, onPartial) {
2314
2325
  return parseOpenAICompatibleTextStream(res, onPartial);
2315
2326
  }
@@ -2321,9 +2332,6 @@ If it's in another language, summarize in that language.
2321
2332
  parseOneShot(data) {
2322
2333
  return parseOpenAICompatibleOneShot(data);
2323
2334
  }
2324
- usesCompatibleChatCompletions() {
2325
- return OPENAI_COMPATIBLE_CHAT_COMPLETIONS_PROVIDERS.has(this.provider);
2326
- }
2327
2335
  };
2328
2336
 
2329
2337
  // src/services/providers/deepseek/DeepSeekChatService.ts
@@ -2495,6 +2503,221 @@ If it's in another language, summarize in that language.
2495
2503
  }
2496
2504
  };
2497
2505
 
2506
+ // src/services/providers/gemini/geminiMessageConverter.ts
2507
+ function mapRoleToGemini(role) {
2508
+ switch (role) {
2509
+ case "system":
2510
+ return "model";
2511
+ case "user":
2512
+ return "user";
2513
+ case "assistant":
2514
+ return "model";
2515
+ default:
2516
+ return "user";
2517
+ }
2518
+ }
2519
+ function convertMessagesToGeminiFormat(messages, options = {}) {
2520
+ const gemini = [];
2521
+ let currentRole = null;
2522
+ let currentParts = [];
2523
+ const pushCurrent = () => {
2524
+ if (currentRole && currentParts.length) {
2525
+ gemini.push({ role: currentRole, parts: [...currentParts] });
2526
+ currentParts = [];
2527
+ }
2528
+ };
2529
+ for (const msg of messages) {
2530
+ const role = mapRoleToGemini(msg.role);
2531
+ if (msg.tool_calls) {
2532
+ pushCurrent();
2533
+ for (const call of msg.tool_calls) {
2534
+ options.callIdMap?.set(call.id, call.function.name);
2535
+ gemini.push({
2536
+ role: "model",
2537
+ parts: [
2538
+ {
2539
+ functionCall: {
2540
+ name: call.function.name,
2541
+ args: JSON.parse(call.function.arguments || "{}")
2542
+ }
2543
+ }
2544
+ ]
2545
+ });
2546
+ }
2547
+ continue;
2548
+ }
2549
+ if (msg.role === "tool") {
2550
+ pushCurrent();
2551
+ const funcName = msg.name ?? options.callIdMap?.get(msg.tool_call_id) ?? "result";
2552
+ gemini.push({
2553
+ role: "user",
2554
+ parts: [
2555
+ {
2556
+ functionResponse: {
2557
+ name: funcName,
2558
+ response: normalizeToolResult(safeJsonParse(msg.content))
2559
+ }
2560
+ }
2561
+ ]
2562
+ });
2563
+ continue;
2564
+ }
2565
+ if (role !== currentRole) pushCurrent();
2566
+ currentRole = role;
2567
+ currentParts.push({ text: msg.content });
2568
+ }
2569
+ pushCurrent();
2570
+ return gemini;
2571
+ }
2572
+ async function convertVisionMessagesToGeminiFormat(messages, options = {}) {
2573
+ const imageFetcher = options.imageFetcher ?? ChatServiceHttpClient.get;
2574
+ const encodeBlob = options.blobToBase64 ?? blobToBase64;
2575
+ const geminiMessages = [];
2576
+ let currentRole = null;
2577
+ let currentParts = [];
2578
+ for (const msg of messages) {
2579
+ const role = mapRoleToGemini(msg.role);
2580
+ if (msg.tool_calls) {
2581
+ for (const call of msg.tool_calls) {
2582
+ geminiMessages.push({
2583
+ role: "model",
2584
+ parts: [
2585
+ {
2586
+ functionCall: {
2587
+ name: call.function.name,
2588
+ args: JSON.parse(call.function.arguments || "{}")
2589
+ }
2590
+ }
2591
+ ]
2592
+ });
2593
+ }
2594
+ continue;
2595
+ }
2596
+ if (msg.role === "tool") {
2597
+ const funcName = msg.name ?? options.callIdMap?.get(msg.tool_call_id) ?? "result";
2598
+ geminiMessages.push({
2599
+ role: "user",
2600
+ parts: [
2601
+ {
2602
+ functionResponse: {
2603
+ name: funcName,
2604
+ response: normalizeToolResult(
2605
+ safeJsonParse(msg.content)
2606
+ )
2607
+ }
2608
+ }
2609
+ ]
2610
+ });
2611
+ continue;
2612
+ }
2613
+ if (role !== currentRole && currentParts.length > 0) {
2614
+ geminiMessages.push({
2615
+ role: currentRole,
2616
+ parts: [...currentParts]
2617
+ });
2618
+ currentParts = [];
2619
+ }
2620
+ currentRole = role;
2621
+ if (typeof msg.content === "string") {
2622
+ currentParts.push({ text: msg.content });
2623
+ } else if (Array.isArray(msg.content)) {
2624
+ for (const block of msg.content) {
2625
+ if (block.type === "text") {
2626
+ currentParts.push({ text: block.text });
2627
+ } else if (block.type === "image_url") {
2628
+ try {
2629
+ const imageResponse = await imageFetcher(block.image_url.url);
2630
+ const imageBlob = await imageResponse.blob();
2631
+ const base64Data = await encodeBlob(imageBlob);
2632
+ currentParts.push({
2633
+ inlineData: {
2634
+ mimeType: imageBlob.type || "image/jpeg",
2635
+ data: base64Data.split(",")[1]
2636
+ }
2637
+ });
2638
+ } catch (error) {
2639
+ console.error("Error processing image:", error);
2640
+ throw new Error(`Failed to process image: ${error.message}`);
2641
+ }
2642
+ }
2643
+ }
2644
+ }
2645
+ }
2646
+ if (currentRole && currentParts.length > 0) {
2647
+ geminiMessages.push({
2648
+ role: currentRole,
2649
+ parts: [...currentParts]
2650
+ });
2651
+ }
2652
+ return geminiMessages;
2653
+ }
2654
+ function safeJsonParse(str) {
2655
+ try {
2656
+ return JSON.parse(str);
2657
+ } catch {
2658
+ return str;
2659
+ }
2660
+ }
2661
+ function normalizeToolResult(val) {
2662
+ if (val === null) return { content: null };
2663
+ if (typeof val === "object") return val;
2664
+ return { content: val };
2665
+ }
2666
+ function blobToBase64(blob) {
2667
+ return new Promise((resolve, reject) => {
2668
+ const reader = new FileReader();
2669
+ reader.onloadend = () => resolve(reader.result);
2670
+ reader.onerror = reject;
2671
+ reader.readAsDataURL(blob);
2672
+ });
2673
+ }
2674
+
2675
+ // src/services/providers/gemini/geminiToolAdapter.ts
2676
+ function buildGeminiToolDeclarations(tools, mcpToolSchemas) {
2677
+ return [...tools, ...mcpToolSchemas].map((tool) => ({
2678
+ name: tool.name,
2679
+ description: tool.description,
2680
+ parameters: tool.parameters
2681
+ }));
2682
+ }
2683
+ function buildGeminiToolConfig(tools, mcpToolSchemas) {
2684
+ const functionDeclarations = buildGeminiToolDeclarations(
2685
+ tools,
2686
+ mcpToolSchemas
2687
+ );
2688
+ if (functionDeclarations.length === 0) {
2689
+ return void 0;
2690
+ }
2691
+ return {
2692
+ tools: [
2693
+ {
2694
+ functionDeclarations
2695
+ }
2696
+ ],
2697
+ toolConfig: {
2698
+ functionCallingConfig: {
2699
+ mode: "AUTO"
2700
+ }
2701
+ }
2702
+ };
2703
+ }
2704
+ function createFallbackMCPToolSchemas(servers) {
2705
+ return servers.map((server) => ({
2706
+ name: `mcp_${server.name}_search`,
2707
+ description: `Search using ${server.name} MCP server (fallback)`,
2708
+ parameters: {
2709
+ type: "object",
2710
+ properties: {
2711
+ query: {
2712
+ type: "string",
2713
+ description: "Search query"
2714
+ }
2715
+ },
2716
+ required: ["query"]
2717
+ }
2718
+ }));
2719
+ }
2720
+
2498
2721
  // src/services/providers/gemini/GeminiChatService.ts
2499
2722
  var GeminiChatService = class {
2500
2723
  /**
@@ -2524,21 +2747,6 @@ If it's in another language, summarize in that language.
2524
2747
  this.tools = tools;
2525
2748
  this.mcpServers = mcpServers;
2526
2749
  }
2527
- /* ────────────────────────────────── */
2528
- /* Utilities */
2529
- /* ────────────────────────────────── */
2530
- safeJsonParse(str) {
2531
- try {
2532
- return JSON.parse(str);
2533
- } catch {
2534
- return str;
2535
- }
2536
- }
2537
- normalizeToolResult(val) {
2538
- if (val === null) return { content: null };
2539
- if (typeof val === "object") return val;
2540
- return { content: val };
2541
- }
2542
2750
  isGemma4Model(model) {
2543
2751
  return /^gemma-4-/.test(model);
2544
2752
  }
@@ -2642,20 +2850,7 @@ If it's in another language, summarize in that language.
2642
2850
  this.mcpSchemasInitialized = true;
2643
2851
  } catch (error) {
2644
2852
  console.warn("Failed to initialize MCP schemas, using fallback:", error);
2645
- this.mcpToolSchemas = this.mcpServers.map((server) => ({
2646
- name: `mcp_${server.name}_search`,
2647
- description: `Search using ${server.name} MCP server (fallback)`,
2648
- parameters: {
2649
- type: "object",
2650
- properties: {
2651
- query: {
2652
- type: "string",
2653
- description: "Search query"
2654
- }
2655
- },
2656
- required: ["query"]
2657
- }
2658
- }));
2853
+ this.mcpToolSchemas = createFallbackMCPToolSchemas(this.mcpServers);
2659
2854
  this.mcpSchemasInitialized = true;
2660
2855
  }
2661
2856
  }
@@ -2715,64 +2910,6 @@ If it's in another language, summarize in that language.
2715
2910
  }
2716
2911
  }
2717
2912
  /* ────────────────────────────────── */
2718
- /* OpenAI → Gemini conversion */
2719
- /* ────────────────────────────────── */
2720
- convertMessagesToGeminiFormat(messages) {
2721
- const gemini = [];
2722
- let currentRole = null;
2723
- let currentParts = [];
2724
- const pushCurrent = () => {
2725
- if (currentRole && currentParts.length) {
2726
- gemini.push({ role: currentRole, parts: [...currentParts] });
2727
- currentParts = [];
2728
- }
2729
- };
2730
- for (const msg of messages) {
2731
- const role = this.mapRoleToGemini(msg.role);
2732
- if (msg.tool_calls) {
2733
- pushCurrent();
2734
- for (const call of msg.tool_calls) {
2735
- this.callIdMap.set(call.id, call.function.name);
2736
- gemini.push({
2737
- role: "model",
2738
- parts: [
2739
- {
2740
- functionCall: {
2741
- name: call.function.name,
2742
- args: JSON.parse(call.function.arguments || "{}")
2743
- }
2744
- }
2745
- ]
2746
- });
2747
- }
2748
- continue;
2749
- }
2750
- if (msg.role === "tool") {
2751
- pushCurrent();
2752
- const funcName = msg.name ?? this.callIdMap.get(msg.tool_call_id) ?? "result";
2753
- gemini.push({
2754
- role: "user",
2755
- parts: [
2756
- {
2757
- functionResponse: {
2758
- name: funcName,
2759
- response: this.normalizeToolResult(
2760
- this.safeJsonParse(msg.content)
2761
- )
2762
- }
2763
- }
2764
- ]
2765
- });
2766
- continue;
2767
- }
2768
- if (role !== currentRole) pushCurrent();
2769
- currentRole = role;
2770
- currentParts.push({ text: msg.content });
2771
- }
2772
- pushCurrent();
2773
- return gemini;
2774
- }
2775
- /* ────────────────────────────────── */
2776
2913
  /* HTTP call */
2777
2914
  /* ────────────────────────────────── */
2778
2915
  async callGemini(messages, model, stream = false, maxTokens) {
@@ -2781,9 +2918,14 @@ If it's in another language, summarize in that language.
2781
2918
  (b) => b?.type === "image_url" || b?.inlineData
2782
2919
  )
2783
2920
  );
2784
- const contents = hasVision ? await this.convertVisionMessagesToGeminiFormat(
2785
- messages
2786
- ) : this.convertMessagesToGeminiFormat(messages);
2921
+ const contents = hasVision ? await convertVisionMessagesToGeminiFormat(
2922
+ messages,
2923
+ {
2924
+ callIdMap: this.callIdMap
2925
+ }
2926
+ ) : convertMessagesToGeminiFormat(messages, {
2927
+ callIdMap: this.callIdMap
2928
+ });
2787
2929
  const body = {
2788
2930
  contents,
2789
2931
  generationConfig: {
@@ -2796,37 +2938,18 @@ If it's in another language, summarize in that language.
2796
2938
  thinkingLevel: "MINIMAL"
2797
2939
  };
2798
2940
  }
2799
- const allToolDeclarations = [];
2800
- if (this.tools.length > 0) {
2801
- allToolDeclarations.push(
2802
- ...this.tools.map((t) => ({
2803
- name: t.name,
2804
- description: t.description,
2805
- parameters: t.parameters
2806
- }))
2807
- );
2808
- }
2941
+ let activeMCPToolSchemas = [];
2809
2942
  if (this.mcpServers.length > 0) {
2810
2943
  try {
2811
2944
  await this.initializeMCPSchemas();
2812
- allToolDeclarations.push(
2813
- ...this.mcpToolSchemas.map((t) => ({
2814
- name: t.name,
2815
- description: t.description,
2816
- parameters: t.parameters
2817
- }))
2818
- );
2945
+ activeMCPToolSchemas = this.mcpToolSchemas;
2819
2946
  } catch (error) {
2820
2947
  console.warn("MCP initialization failed, skipping MCP tools:", error);
2821
2948
  }
2822
2949
  }
2823
- if (allToolDeclarations.length > 0) {
2824
- body.tools = [
2825
- {
2826
- functionDeclarations: allToolDeclarations
2827
- }
2828
- ];
2829
- body.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
2950
+ const toolConfig = buildGeminiToolConfig(this.tools, activeMCPToolSchemas);
2951
+ if (toolConfig) {
2952
+ Object.assign(body, toolConfig);
2830
2953
  }
2831
2954
  const fetchOnce = async (ver, payload) => {
2832
2955
  const fn = stream ? "streamGenerateContent" : "generateContent";
@@ -2863,125 +2986,6 @@ If it's in another language, summarize in that language.
2863
2986
  throw error;
2864
2987
  }
2865
2988
  }
2866
- /**
2867
- * Convert AITuber OnAir vision messages to Gemini format
2868
- * @param messages Array of vision messages
2869
- * @returns Gemini formatted vision messages
2870
- */
2871
- async convertVisionMessagesToGeminiFormat(messages) {
2872
- const geminiMessages = [];
2873
- let currentRole = null;
2874
- let currentParts = [];
2875
- for (const msg of messages) {
2876
- const role = this.mapRoleToGemini(msg.role);
2877
- if (msg.tool_calls) {
2878
- for (const call of msg.tool_calls) {
2879
- geminiMessages.push({
2880
- role: "model",
2881
- parts: [
2882
- {
2883
- functionCall: {
2884
- name: call.function.name,
2885
- args: JSON.parse(call.function.arguments || "{}")
2886
- }
2887
- }
2888
- ]
2889
- });
2890
- }
2891
- continue;
2892
- }
2893
- if (msg.role === "tool") {
2894
- const funcName = msg.name ?? this.callIdMap.get(msg.tool_call_id) ?? "result";
2895
- geminiMessages.push({
2896
- role: "user",
2897
- parts: [
2898
- {
2899
- functionResponse: {
2900
- name: funcName,
2901
- response: this.normalizeToolResult(
2902
- this.safeJsonParse(msg.content)
2903
- )
2904
- }
2905
- }
2906
- ]
2907
- });
2908
- continue;
2909
- }
2910
- if (role !== currentRole && currentParts.length > 0) {
2911
- geminiMessages.push({
2912
- role: currentRole,
2913
- parts: [...currentParts]
2914
- });
2915
- currentParts = [];
2916
- }
2917
- currentRole = role;
2918
- if (typeof msg.content === "string") {
2919
- currentParts.push({ text: msg.content });
2920
- } else if (Array.isArray(msg.content)) {
2921
- for (const block of msg.content) {
2922
- if (block.type === "text") {
2923
- currentParts.push({ text: block.text });
2924
- } else if (block.type === "image_url") {
2925
- try {
2926
- const imageResponse = await ChatServiceHttpClient.get(
2927
- block.image_url.url
2928
- );
2929
- const imageBlob = await imageResponse.blob();
2930
- const base64Data = await this.blobToBase64(imageBlob);
2931
- currentParts.push({
2932
- inlineData: {
2933
- mimeType: imageBlob.type || "image/jpeg",
2934
- data: base64Data.split(",")[1]
2935
- // Remove the "data:image/jpeg;base64," prefix
2936
- }
2937
- });
2938
- } catch (error) {
2939
- console.error("Error processing image:", error);
2940
- throw new Error(`Failed to process image: ${error.message}`);
2941
- }
2942
- }
2943
- }
2944
- }
2945
- }
2946
- if (currentRole && currentParts.length > 0) {
2947
- geminiMessages.push({
2948
- role: currentRole,
2949
- parts: [...currentParts]
2950
- });
2951
- }
2952
- return geminiMessages;
2953
- }
2954
- /**
2955
- * Convert Blob to Base64 string
2956
- * @param blob Image blob
2957
- * @returns Promise with base64 encoded string
2958
- */
2959
- blobToBase64(blob) {
2960
- return new Promise((resolve, reject) => {
2961
- const reader = new FileReader();
2962
- reader.onloadend = () => resolve(reader.result);
2963
- reader.onerror = reject;
2964
- reader.readAsDataURL(blob);
2965
- });
2966
- }
2967
- /**
2968
- * Map AITuber OnAir roles to Gemini roles
2969
- * @param role AITuber OnAir role
2970
- * @returns Gemini role
2971
- */
2972
- mapRoleToGemini(role) {
2973
- switch (role) {
2974
- case "system":
2975
- return "model";
2976
- // Gemini uses 'model' for system messages
2977
- case "user":
2978
- return "user";
2979
- case "assistant":
2980
- return "model";
2981
- default:
2982
- return "user";
2983
- }
2984
- }
2985
2989
  /* ────────────────────────────────────────────────────────── */
2986
2990
  /* Convert NDJSON stream to common format */
2987
2991
  /* ────────────────────────────────────────────────────────── */