@aituber-onair/chat 0.34.1 → 0.36.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 +9 -3
  2. package/README.md +10 -4
  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 +1 -0
  8. package/dist/cjs/constants/gemini.d.ts.map +1 -1
  9. package/dist/cjs/constants/gemini.js +3 -1
  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 +1 -21
  23. package/dist/cjs/services/providers/gemini/GeminiChatService.d.ts.map +1 -1
  24. package/dist/cjs/services/providers/gemini/GeminiChatService.js +21 -258
  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 +1 -0
  51. package/dist/esm/constants/gemini.d.ts.map +1 -1
  52. package/dist/esm/constants/gemini.js +2 -0
  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 +1 -21
  66. package/dist/esm/services/providers/gemini/GeminiChatService.d.ts.map +1 -1
  67. package/dist/esm/services/providers/gemini/GeminiChatService.js +22 -259
  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 +561 -545
  90. package/dist/umd/aituber-onair-chat.min.js +10 -10
  91. package/package.json +3 -1
@@ -84,6 +84,7 @@ 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,
@@ -99,6 +100,7 @@ var AITuberOnAirChat = (() => {
99
100
  MODEL_GEMINI_3_1_FLASH_LITE: () => MODEL_GEMINI_3_1_FLASH_LITE,
100
101
  MODEL_GEMINI_3_1_FLASH_LITE_PREVIEW: () => MODEL_GEMINI_3_1_FLASH_LITE_PREVIEW,
101
102
  MODEL_GEMINI_3_1_PRO_PREVIEW: () => MODEL_GEMINI_3_1_PRO_PREVIEW,
103
+ MODEL_GEMINI_3_5_FLASH: () => MODEL_GEMINI_3_5_FLASH,
102
104
  MODEL_GEMINI_3_FLASH_PREVIEW: () => MODEL_GEMINI_3_FLASH_PREVIEW,
103
105
  MODEL_GEMINI_3_PRO_PREVIEW: () => MODEL_GEMINI_3_PRO_PREVIEW,
104
106
  MODEL_GEMINI_NANO: () => MODEL_GEMINI_NANO,
@@ -296,6 +298,7 @@ var AITuberOnAirChat = (() => {
296
298
  var ENDPOINT_GEMINI_API = "https://generativelanguage.googleapis.com";
297
299
  var MODEL_GEMMA_4_31B_IT = "gemma-4-31b-it";
298
300
  var MODEL_GEMMA_4_26B_A4B_IT = "gemma-4-26b-a4b-it";
301
+ var MODEL_GEMINI_3_5_FLASH = "gemini-3.5-flash";
299
302
  var MODEL_GEMINI_3_1_PRO_PREVIEW = "gemini-3.1-pro-preview";
300
303
  var MODEL_GEMINI_3_1_FLASH_LITE = "gemini-3.1-flash-lite";
301
304
  var MODEL_GEMINI_3_1_FLASH_LITE_PREVIEW = "gemini-3.1-flash-lite-preview";
@@ -308,6 +311,7 @@ var AITuberOnAirChat = (() => {
308
311
  var MODEL_GEMINI_2_0_FLASH = "gemini-2.0-flash";
309
312
  var MODEL_GEMINI_2_0_FLASH_LITE = "gemini-2.0-flash-lite";
310
313
  var GEMINI_RECOMMENDED_MODELS = [
314
+ MODEL_GEMINI_3_5_FLASH,
311
315
  MODEL_GEMINI_3_1_FLASH_LITE,
312
316
  MODEL_GEMINI_3_1_PRO_PREVIEW,
313
317
  MODEL_GEMINI_3_FLASH_PREVIEW,
@@ -343,6 +347,7 @@ var AITuberOnAirChat = (() => {
343
347
  var MODEL_CLAUDE_4_6_SONNET = "claude-sonnet-4-6";
344
348
  var MODEL_CLAUDE_4_6_OPUS = "claude-opus-4-6";
345
349
  var MODEL_CLAUDE_4_7_OPUS = "claude-opus-4-7";
350
+ var MODEL_CLAUDE_4_8_OPUS = "claude-opus-4-8";
346
351
  var CLAUDE_VISION_SUPPORTED_MODELS = [
347
352
  MODEL_CLAUDE_3_HAIKU,
348
353
  MODEL_CLAUDE_4_SONNET,
@@ -352,7 +357,8 @@ var AITuberOnAirChat = (() => {
352
357
  MODEL_CLAUDE_4_5_OPUS,
353
358
  MODEL_CLAUDE_4_6_SONNET,
354
359
  MODEL_CLAUDE_4_6_OPUS,
355
- MODEL_CLAUDE_4_7_OPUS
360
+ MODEL_CLAUDE_4_7_OPUS,
361
+ MODEL_CLAUDE_4_8_OPUS
356
362
  ];
357
363
 
358
364
  // src/constants/openrouter.ts
@@ -1244,6 +1250,94 @@ If it's in another language, summarize in that language.
1244
1250
  };
1245
1251
  }
1246
1252
 
1253
+ // src/services/providers/claude/claudeMessageConverter.ts
1254
+ function convertMessagesToClaudeFormat(messages) {
1255
+ return messages.map((msg) => ({
1256
+ role: mapRoleToClaude(msg.role),
1257
+ content: msg.content
1258
+ }));
1259
+ }
1260
+ function convertVisionMessagesToClaudeFormat(messages) {
1261
+ return messages.map((msg) => {
1262
+ if (typeof msg.content === "string") {
1263
+ return {
1264
+ role: mapRoleToClaude(msg.role),
1265
+ content: [
1266
+ {
1267
+ type: "text",
1268
+ text: msg.content
1269
+ }
1270
+ ]
1271
+ };
1272
+ }
1273
+ if (Array.isArray(msg.content)) {
1274
+ const content = msg.content.map((block) => {
1275
+ if (block.type === "image_url") {
1276
+ if (block.image_url.url.startsWith("data:")) {
1277
+ const m = block.image_url.url.match(/^data:([^;]+);base64,(.+)$/);
1278
+ if (m) {
1279
+ return {
1280
+ type: "image",
1281
+ source: {
1282
+ type: "base64",
1283
+ media_type: m[1],
1284
+ data: m[2]
1285
+ }
1286
+ };
1287
+ }
1288
+ return null;
1289
+ }
1290
+ return {
1291
+ type: "image",
1292
+ source: {
1293
+ type: "url",
1294
+ url: block.image_url.url,
1295
+ media_type: getMimeTypeFromUrl(block.image_url.url)
1296
+ }
1297
+ };
1298
+ }
1299
+ return block;
1300
+ }).filter((b) => b);
1301
+ return {
1302
+ role: mapRoleToClaude(msg.role),
1303
+ content
1304
+ };
1305
+ }
1306
+ return {
1307
+ role: mapRoleToClaude(msg.role),
1308
+ content: []
1309
+ };
1310
+ });
1311
+ }
1312
+ function mapRoleToClaude(role) {
1313
+ switch (role) {
1314
+ case "system":
1315
+ return "system";
1316
+ case "user":
1317
+ return "user";
1318
+ case "assistant":
1319
+ return "assistant";
1320
+ default:
1321
+ return "user";
1322
+ }
1323
+ }
1324
+ function getMimeTypeFromUrl(url) {
1325
+ const extension = url.split(".").pop()?.toLowerCase();
1326
+ switch (extension) {
1327
+ case "jpg":
1328
+ case "jpeg":
1329
+ return "image/jpeg";
1330
+ case "png":
1331
+ return "image/png";
1332
+ case "gif":
1333
+ return "image/gif";
1334
+ case "webp":
1335
+ return "image/webp";
1336
+ default:
1337
+ return "image/jpeg";
1338
+ }
1339
+ }
1340
+
1247
1341
  // src/services/providers/claude/ClaudeChatService.ts
1248
1342
  var ClaudeChatService = class {
1249
1343
  /**
@@ -1350,112 +1444,6 @@ If it's in another language, summarize in that language.
1350
1444
  toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
1351
1445
  });
1352
1446
  }
1353
- /**
1354
- * Convert AITuber OnAir messages to Claude format
1355
- * @param messages Array of messages
1356
- * @returns Claude formatted messages
1357
- */
1358
- convertMessagesToClaudeFormat(messages) {
1359
- return messages.map((msg) => {
1360
- return {
1361
- role: this.mapRoleToClaude(msg.role),
1362
- content: msg.content
1363
- };
1364
- });
1365
- }
1366
- /**
1367
- * Convert AITuber OnAir vision messages to Claude format
1368
- * @param messages Array of vision messages
1369
- * @returns Claude formatted vision messages
1370
- */
1371
- convertVisionMessagesToClaudeFormat(messages) {
1372
- return messages.map((msg) => {
1373
- if (typeof msg.content === "string") {
1374
- return {
1375
- role: this.mapRoleToClaude(msg.role),
1376
- content: [
1377
- {
1378
- type: "text",
1379
- text: msg.content
1380
- }
1381
- ]
1382
- };
1383
- }
1384
- if (Array.isArray(msg.content)) {
1385
- const content = msg.content.map((block) => {
1386
- if (block.type === "image_url") {
1387
- if (block.image_url.url.startsWith("data:")) {
1388
- const m = block.image_url.url.match(
1389
- /^data:([^;]+);base64,(.+)$/
1390
- );
1391
- if (m) {
1392
- return {
1393
- type: "image",
1394
- source: { type: "base64", media_type: m[1], data: m[2] }
1395
- };
1396
- }
1397
- return null;
1398
- }
1399
- return {
1400
- type: "image",
1401
- source: {
1402
- type: "url",
1403
- url: block.image_url.url,
1404
- media_type: this.getMimeTypeFromUrl(block.image_url.url)
1405
- }
1406
- };
1407
- }
1408
- return block;
1409
- }).filter((b) => b);
1410
- return {
1411
- role: this.mapRoleToClaude(msg.role),
1412
- content
1413
- };
1414
- }
1415
- return {
1416
- role: this.mapRoleToClaude(msg.role),
1417
- content: []
1418
- };
1419
- });
1420
- }
1421
- /**
1422
- * Map AITuber OnAir roles to Claude roles
1423
- * @param role AITuber OnAir role
1424
- * @returns Claude role
1425
- */
1426
- mapRoleToClaude(role) {
1427
- switch (role) {
1428
- case "system":
1429
- return "system";
1430
- case "user":
1431
- return "user";
1432
- case "assistant":
1433
- return "assistant";
1434
- default:
1435
- return "user";
1436
- }
1437
- }
1438
- /**
1439
- * Get MIME type from URL
1440
- * @param url Image URL
1441
- * @returns MIME type
1442
- */
1443
- getMimeTypeFromUrl(url) {
1444
- const extension = url.split(".").pop()?.toLowerCase();
1445
- switch (extension) {
1446
- case "jpg":
1447
- case "jpeg":
1448
- return "image/jpeg";
1449
- case "png":
1450
- return "image/png";
1451
- case "gif":
1452
- return "image/gif";
1453
- case "webp":
1454
- return "image/webp";
1455
- default:
1456
- return "image/jpeg";
1457
- }
1458
- }
1459
1447
  /**
1460
1448
  * Call Claude API
1461
1449
  * @param messages Array of messages to send
@@ -1475,9 +1463,7 @@ If it's in another language, summarize in that language.
1475
1463
  const body = {
1476
1464
  model,
1477
1465
  system,
1478
- messages: hasVision ? this.convertVisionMessagesToClaudeFormat(
1479
- content
1480
- ) : this.convertMessagesToClaudeFormat(content),
1466
+ messages: hasVision ? convertVisionMessagesToClaudeFormat(content) : convertMessagesToClaudeFormat(content),
1481
1467
  stream,
1482
1468
  max_tokens: maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength)
1483
1469
  };
@@ -1740,6 +1726,7 @@ If it's in another language, summarize in that language.
1740
1726
  MODEL_CLAUDE_4_6_SONNET,
1741
1727
  MODEL_CLAUDE_4_6_OPUS,
1742
1728
  MODEL_CLAUDE_4_7_OPUS,
1729
+ MODEL_CLAUDE_4_8_OPUS,
1743
1730
  MODEL_CLAUDE_3_HAIKU
1744
1731
  ];
1745
1732
  }
@@ -1931,7 +1918,49 @@ If it's in another language, summarize in that language.
1931
1918
  };
1932
1919
  }
1933
1920
 
1934
- // src/services/providers/openai/OpenAIChatService.ts
1921
+ // src/services/providers/openai/openaiToolBuilder.ts
1922
+ function buildOpenAIToolsDefinition({
1923
+ tools,
1924
+ mcpServers,
1925
+ isResponsesAPI
1926
+ }) {
1927
+ const toolDefs = [];
1928
+ if (tools.length > 0) {
1929
+ toolDefs.push(
1930
+ ...buildOpenAICompatibleTools(
1931
+ tools,
1932
+ isResponsesAPI ? "responses" : "chat-completions"
1933
+ )
1934
+ );
1935
+ }
1936
+ if (mcpServers.length > 0 && isResponsesAPI) {
1937
+ toolDefs.push(...buildOpenAIMCPToolsDefinition(mcpServers));
1938
+ }
1939
+ return toolDefs;
1940
+ }
1941
+ function buildOpenAIMCPToolsDefinition(mcpServers) {
1942
+ return mcpServers.map((server) => {
1943
+ const mcpDef = {
1944
+ type: "mcp",
1945
+ server_label: server.name,
1946
+ server_url: server.url
1947
+ };
1948
+ if (server.require_approval) {
1949
+ mcpDef.require_approval = server.require_approval;
1950
+ }
1951
+ if (server.tool_configuration?.allowed_tools) {
1952
+ mcpDef.allowed_tools = server.tool_configuration.allowed_tools;
1953
+ }
1954
+ if (server.authorization_token) {
1955
+ mcpDef.headers = {
1956
+ Authorization: `Bearer ${server.authorization_token}`
1957
+ };
1958
+ }
1959
+ return mcpDef;
1960
+ });
1961
+ }
1962
+
1963
+ // src/services/providers/openai/openaiRequestBuilder.ts
1935
1964
  var GPT5_RESPONSE_LENGTH_MIN_TOKENS = {
1936
1965
  [CHAT_RESPONSE_LENGTH.VERY_SHORT]: 800,
1937
1966
  [CHAT_RESPONSE_LENGTH.SHORT]: 1200,
@@ -1953,6 +1982,177 @@ If it's in another language, summarize in that language.
1953
1982
  "deepseek",
1954
1983
  "mistral"
1955
1984
  ]);
1985
+ function buildOpenAIRequestBody({
1986
+ provider,
1987
+ endpoint,
1988
+ messages,
1989
+ model,
1990
+ stream,
1991
+ tools,
1992
+ mcpServers,
1993
+ responseLength,
1994
+ verbosity,
1995
+ reasoning_effort,
1996
+ enableReasoningSummary,
1997
+ maxTokens
1998
+ }) {
1999
+ const isResponsesAPI = endpoint === ENDPOINT_OPENAI_RESPONSES_API;
2000
+ validateMCPCompatibility(endpoint, mcpServers);
2001
+ const body = {
2002
+ model,
2003
+ stream
2004
+ };
2005
+ const tokenLimit = resolveOpenAITokenLimit({
2006
+ provider,
2007
+ model,
2008
+ responseLength,
2009
+ reasoning_effort,
2010
+ maxTokens
2011
+ });
2012
+ if (isResponsesAPI) {
2013
+ if (tokenLimit !== void 0) {
2014
+ body.max_output_tokens = tokenLimit;
2015
+ }
2016
+ } else {
2017
+ if (tokenLimit !== void 0) {
2018
+ if (usesCompatibleChatCompletions(provider)) {
2019
+ body.max_tokens = tokenLimit;
2020
+ } else {
2021
+ body.max_completion_tokens = tokenLimit;
2022
+ }
2023
+ }
2024
+ }
2025
+ if (isResponsesAPI) {
2026
+ body.input = cleanMessagesForResponsesAPI(messages);
2027
+ } else {
2028
+ body.messages = provider === "mistral" ? cleanMessagesForMistralChatCompletions(messages) : messages;
2029
+ }
2030
+ if (isGPT5Model(model)) {
2031
+ if (isResponsesAPI) {
2032
+ if (reasoning_effort) {
2033
+ body.reasoning = {
2034
+ ...body.reasoning,
2035
+ effort: reasoning_effort
2036
+ };
2037
+ if (enableReasoningSummary) {
2038
+ body.reasoning.summary = "auto";
2039
+ }
2040
+ }
2041
+ if (verbosity) {
2042
+ body.text = {
2043
+ ...body.text,
2044
+ format: { type: "text" },
2045
+ verbosity
2046
+ };
2047
+ }
2048
+ } else {
2049
+ if (reasoning_effort) {
2050
+ body.reasoning_effort = reasoning_effort;
2051
+ }
2052
+ if (verbosity) {
2053
+ body.verbosity = verbosity;
2054
+ }
2055
+ }
2056
+ }
2057
+ if (provider === "mistral" && isMistralReasoningEffortModel(model) && reasoning_effort && isMistralReasoningEffort(reasoning_effort)) {
2058
+ body.reasoning_effort = reasoning_effort;
2059
+ }
2060
+ const toolDefinitions = buildOpenAIToolsDefinition({
2061
+ tools,
2062
+ mcpServers,
2063
+ isResponsesAPI
2064
+ });
2065
+ if (toolDefinitions.length > 0) {
2066
+ body.tools = toolDefinitions;
2067
+ if (!isResponsesAPI) {
2068
+ body.tool_choice = "auto";
2069
+ }
2070
+ }
2071
+ return body;
2072
+ }
2073
+ function resolveOpenAITokenLimit({
2074
+ provider,
2075
+ model,
2076
+ responseLength,
2077
+ reasoning_effort,
2078
+ maxTokens
2079
+ }) {
2080
+ if (maxTokens !== void 0) {
2081
+ return maxTokens;
2082
+ }
2083
+ const baseTokenLimit = usesCompatibleChatCompletions(provider) ? responseLength !== void 0 ? getMaxTokensForResponseLength(responseLength) : void 0 : getMaxTokensForResponseLength(responseLength);
2084
+ if (provider !== "openai" || !isGPT5Model(model) || responseLength === void 0) {
2085
+ return baseTokenLimit;
2086
+ }
2087
+ const effectiveReasoningEffort = reasoning_effort ?? getDefaultReasoningEffortForGPT5Model(model);
2088
+ return Math.max(
2089
+ baseTokenLimit ?? 0,
2090
+ GPT5_RESPONSE_LENGTH_MIN_TOKENS[responseLength],
2091
+ GPT5_REASONING_MIN_TOKENS[effectiveReasoningEffort]
2092
+ );
2093
+ }
2094
+ function validateMCPCompatibility(endpoint, mcpServers) {
2095
+ if (mcpServers.length > 0 && endpoint === ENDPOINT_OPENAI_CHAT_COMPLETIONS_API) {
2096
+ throw new Error(
2097
+ `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.`
2098
+ );
2099
+ }
2100
+ }
2101
+ function cleanMessagesForResponsesAPI(messages) {
2102
+ return messages.map((msg) => {
2103
+ const role = msg.role === "tool" ? "user" : msg.role;
2104
+ const cleanMsg = {
2105
+ role
2106
+ };
2107
+ if (typeof msg.content === "string") {
2108
+ cleanMsg.content = msg.content;
2109
+ } else if (Array.isArray(msg.content)) {
2110
+ cleanMsg.content = msg.content.map((block) => {
2111
+ if (block.type === "text") {
2112
+ return {
2113
+ type: "input_text",
2114
+ text: block.text
2115
+ };
2116
+ } else if (block.type === "image_url") {
2117
+ return {
2118
+ type: "input_image",
2119
+ image_url: block.image_url.url
2120
+ };
2121
+ }
2122
+ return block;
2123
+ });
2124
+ } else {
2125
+ cleanMsg.content = msg.content;
2126
+ }
2127
+ return cleanMsg;
2128
+ });
2129
+ }
2130
+ function cleanMessagesForMistralChatCompletions(messages) {
2131
+ return messages.map((msg) => {
2132
+ const cleanMsg = {
2133
+ role: msg.role
2134
+ };
2135
+ if (!Array.isArray(msg.content)) {
2136
+ cleanMsg.content = msg.content;
2137
+ return cleanMsg;
2138
+ }
2139
+ cleanMsg.content = msg.content.map((block) => {
2140
+ if (block.type === "image_url" && typeof block.image_url === "object" && typeof block.image_url?.url === "string") {
2141
+ return {
2142
+ type: "image_url",
2143
+ image_url: block.image_url.url
2144
+ };
2145
+ }
2146
+ return block;
2147
+ });
2148
+ return cleanMsg;
2149
+ });
2150
+ }
2151
+ function usesCompatibleChatCompletions(provider) {
2152
+ return OPENAI_COMPATIBLE_CHAT_COMPLETIONS_PROVIDERS.has(provider);
2153
+ }
2154
+
2155
+ // src/services/providers/openai/OpenAIChatService.ts
1956
2156
  var OpenAIChatService = class {
1957
2157
  /**
1958
2158
  * Constructor
@@ -2105,7 +2305,20 @@ If it's in another language, summarize in that language.
2105
2305
  return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
2106
2306
  }
2107
2307
  async callOpenAI(messages, model, stream = false, maxTokens) {
2108
- const body = this.buildRequestBody(messages, model, stream, maxTokens);
2308
+ const body = buildOpenAIRequestBody({
2309
+ provider: this.provider,
2310
+ endpoint: this.endpoint,
2311
+ messages,
2312
+ model,
2313
+ stream,
2314
+ tools: this.tools,
2315
+ mcpServers: this.mcpServers,
2316
+ responseLength: this.responseLength,
2317
+ verbosity: this.verbosity,
2318
+ reasoning_effort: this.reasoning_effort,
2319
+ enableReasoningSummary: this.enableReasoningSummary,
2320
+ maxTokens
2321
+ });
2109
2322
  const headers = {};
2110
2323
  const shouldSendAuthorization = this.provider !== "openai-compatible" || this.apiKey.trim() !== "";
2111
2324
  if (shouldSendAuthorization) {
@@ -2114,199 +2327,6 @@ If it's in another language, summarize in that language.
2114
2327
  const res = await ChatServiceHttpClient.post(this.endpoint, body, headers);
2115
2328
  return res;
2116
2329
  }
2117
- /**
2118
- * Build request body based on the endpoint type
2119
- */
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
2126
- };
2127
- const tokenLimit = this.resolveTokenLimit(model, maxTokens);
2128
- if (isResponsesAPI) {
2129
- if (tokenLimit !== void 0) {
2130
- body.max_output_tokens = tokenLimit;
2131
- }
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
- }
2139
- }
2140
- }
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
- }
2171
- }
2172
- }
2173
- if (this.provider === "mistral" && isMistralReasoningEffortModel(model) && this.reasoning_effort && isMistralReasoningEffort(this.reasoning_effort)) {
2174
- body.reasoning_effort = this.reasoning_effort;
2175
- }
2176
- const tools = this.buildToolsDefinition();
2177
- if (tools.length > 0) {
2178
- body.tools = tools;
2179
- if (!isResponsesAPI) {
2180
- body.tool_choice = "auto";
2181
- }
2182
- }
2183
- return body;
2184
- }
2185
- resolveTokenLimit(model, maxTokens) {
2186
- if (maxTokens !== void 0) {
2187
- return maxTokens;
2188
- }
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;
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
- );
2199
- }
2200
- /**
2201
- * Validate MCP servers compatibility with the current endpoint
2202
- */
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
- };
2219
- if (typeof msg.content === "string") {
2220
- cleanMsg.content = msg.content;
2221
- } else if (Array.isArray(msg.content)) {
2222
- cleanMsg.content = msg.content.map((block) => {
2223
- if (block.type === "text") {
2224
- return {
2225
- type: "input_text",
2226
- text: block.text
2227
- };
2228
- } else if (block.type === "image_url") {
2229
- return {
2230
- type: "input_image",
2231
- image_url: block.image_url.url
2232
- // Extract the URL string directly
2233
- };
2234
- }
2235
- return block;
2236
- });
2237
- } else {
2238
- cleanMsg.content = msg.content;
2239
- }
2240
- return cleanMsg;
2241
- });
2242
- }
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;
2262
- });
2263
- }
2264
- /**
2265
- * Build tools definition based on the endpoint type
2266
- */
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());
2280
- }
2281
- return toolDefs;
2282
- }
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;
2298
- }
2299
- if (server.tool_configuration?.allowed_tools) {
2300
- mcpDef.allowed_tools = server.tool_configuration.allowed_tools;
2301
- }
2302
- if (server.authorization_token) {
2303
- mcpDef.headers = {
2304
- Authorization: `Bearer ${server.authorization_token}`
2305
- };
2306
- }
2307
- return mcpDef;
2308
- });
2309
- }
2310
2330
  async handleStream(res, onPartial) {
2311
2331
  return parseOpenAICompatibleTextStream(res, onPartial);
2312
2332
  }
@@ -2318,9 +2338,6 @@ If it's in another language, summarize in that language.
2318
2338
  parseOneShot(data) {
2319
2339
  return parseOpenAICompatibleOneShot(data);
2320
2340
  }
2321
- usesCompatibleChatCompletions() {
2322
- return OPENAI_COMPATIBLE_CHAT_COMPLETIONS_PROVIDERS.has(this.provider);
2323
- }
2324
2341
  };
2325
2342
 
2326
2343
  // src/services/providers/deepseek/DeepSeekChatService.ts
@@ -2492,6 +2509,221 @@ If it's in another language, summarize in that language.
2492
2509
  }
2493
2510
  };
2494
2511
 
2512
+ // src/services/providers/gemini/geminiMessageConverter.ts
2513
+ function mapRoleToGemini(role) {
2514
+ switch (role) {
2515
+ case "system":
2516
+ return "model";
2517
+ case "user":
2518
+ return "user";
2519
+ case "assistant":
2520
+ return "model";
2521
+ default:
2522
+ return "user";
2523
+ }
2524
+ }
2525
+ function convertMessagesToGeminiFormat(messages, options = {}) {
2526
+ const gemini = [];
2527
+ let currentRole = null;
2528
+ let currentParts = [];
2529
+ const pushCurrent = () => {
2530
+ if (currentRole && currentParts.length) {
2531
+ gemini.push({ role: currentRole, parts: [...currentParts] });
2532
+ currentParts = [];
2533
+ }
2534
+ };
2535
+ for (const msg of messages) {
2536
+ const role = mapRoleToGemini(msg.role);
2537
+ if (msg.tool_calls) {
2538
+ pushCurrent();
2539
+ for (const call of msg.tool_calls) {
2540
+ options.callIdMap?.set(call.id, call.function.name);
2541
+ gemini.push({
2542
+ role: "model",
2543
+ parts: [
2544
+ {
2545
+ functionCall: {
2546
+ name: call.function.name,
2547
+ args: JSON.parse(call.function.arguments || "{}")
2548
+ }
2549
+ }
2550
+ ]
2551
+ });
2552
+ }
2553
+ continue;
2554
+ }
2555
+ if (msg.role === "tool") {
2556
+ pushCurrent();
2557
+ const funcName = msg.name ?? options.callIdMap?.get(msg.tool_call_id) ?? "result";
2558
+ gemini.push({
2559
+ role: "user",
2560
+ parts: [
2561
+ {
2562
+ functionResponse: {
2563
+ name: funcName,
2564
+ response: normalizeToolResult(safeJsonParse(msg.content))
2565
+ }
2566
+ }
2567
+ ]
2568
+ });
2569
+ continue;
2570
+ }
2571
+ if (role !== currentRole) pushCurrent();
2572
+ currentRole = role;
2573
+ currentParts.push({ text: msg.content });
2574
+ }
2575
+ pushCurrent();
2576
+ return gemini;
2577
+ }
2578
+ async function convertVisionMessagesToGeminiFormat(messages, options = {}) {
2579
+ const imageFetcher = options.imageFetcher ?? ChatServiceHttpClient.get;
2580
+ const encodeBlob = options.blobToBase64 ?? blobToBase64;
2581
+ const geminiMessages = [];
2582
+ let currentRole = null;
2583
+ let currentParts = [];
2584
+ for (const msg of messages) {
2585
+ const role = mapRoleToGemini(msg.role);
2586
+ if (msg.tool_calls) {
2587
+ for (const call of msg.tool_calls) {
2588
+ geminiMessages.push({
2589
+ role: "model",
2590
+ parts: [
2591
+ {
2592
+ functionCall: {
2593
+ name: call.function.name,
2594
+ args: JSON.parse(call.function.arguments || "{}")
2595
+ }
2596
+ }
2597
+ ]
2598
+ });
2599
+ }
2600
+ continue;
2601
+ }
2602
+ if (msg.role === "tool") {
2603
+ const funcName = msg.name ?? options.callIdMap?.get(msg.tool_call_id) ?? "result";
2604
+ geminiMessages.push({
2605
+ role: "user",
2606
+ parts: [
2607
+ {
2608
+ functionResponse: {
2609
+ name: funcName,
2610
+ response: normalizeToolResult(
2611
+ safeJsonParse(msg.content)
2612
+ )
2613
+ }
2614
+ }
2615
+ ]
2616
+ });
2617
+ continue;
2618
+ }
2619
+ if (role !== currentRole && currentParts.length > 0) {
2620
+ geminiMessages.push({
2621
+ role: currentRole,
2622
+ parts: [...currentParts]
2623
+ });
2624
+ currentParts = [];
2625
+ }
2626
+ currentRole = role;
2627
+ if (typeof msg.content === "string") {
2628
+ currentParts.push({ text: msg.content });
2629
+ } else if (Array.isArray(msg.content)) {
2630
+ for (const block of msg.content) {
2631
+ if (block.type === "text") {
2632
+ currentParts.push({ text: block.text });
2633
+ } else if (block.type === "image_url") {
2634
+ try {
2635
+ const imageResponse = await imageFetcher(block.image_url.url);
2636
+ const imageBlob = await imageResponse.blob();
2637
+ const base64Data = await encodeBlob(imageBlob);
2638
+ currentParts.push({
2639
+ inlineData: {
2640
+ mimeType: imageBlob.type || "image/jpeg",
2641
+ data: base64Data.split(",")[1]
2642
+ }
2643
+ });
2644
+ } catch (error) {
2645
+ console.error("Error processing image:", error);
2646
+ throw new Error(`Failed to process image: ${error.message}`);
2647
+ }
2648
+ }
2649
+ }
2650
+ }
2651
+ }
2652
+ if (currentRole && currentParts.length > 0) {
2653
+ geminiMessages.push({
2654
+ role: currentRole,
2655
+ parts: [...currentParts]
2656
+ });
2657
+ }
2658
+ return geminiMessages;
2659
+ }
2660
+ function safeJsonParse(str) {
2661
+ try {
2662
+ return JSON.parse(str);
2663
+ } catch {
2664
+ return str;
2665
+ }
2666
+ }
2667
+ function normalizeToolResult(val) {
2668
+ if (val === null) return { content: null };
2669
+ if (typeof val === "object") return val;
2670
+ return { content: val };
2671
+ }
2672
+ function blobToBase64(blob) {
2673
+ return new Promise((resolve, reject) => {
2674
+ const reader = new FileReader();
2675
+ reader.onloadend = () => resolve(reader.result);
2676
+ reader.onerror = reject;
2677
+ reader.readAsDataURL(blob);
2678
+ });
2679
+ }
2680
+
2681
+ // src/services/providers/gemini/geminiToolAdapter.ts
2682
+ function buildGeminiToolDeclarations(tools, mcpToolSchemas) {
2683
+ return [...tools, ...mcpToolSchemas].map((tool) => ({
2684
+ name: tool.name,
2685
+ description: tool.description,
2686
+ parameters: tool.parameters
2687
+ }));
2688
+ }
2689
+ function buildGeminiToolConfig(tools, mcpToolSchemas) {
2690
+ const functionDeclarations = buildGeminiToolDeclarations(
2691
+ tools,
2692
+ mcpToolSchemas
2693
+ );
2694
+ if (functionDeclarations.length === 0) {
2695
+ return void 0;
2696
+ }
2697
+ return {
2698
+ tools: [
2699
+ {
2700
+ functionDeclarations
2701
+ }
2702
+ ],
2703
+ toolConfig: {
2704
+ functionCallingConfig: {
2705
+ mode: "AUTO"
2706
+ }
2707
+ }
2708
+ };
2709
+ }
2710
+ function createFallbackMCPToolSchemas(servers) {
2711
+ return servers.map((server) => ({
2712
+ name: `mcp_${server.name}_search`,
2713
+ description: `Search using ${server.name} MCP server (fallback)`,
2714
+ parameters: {
2715
+ type: "object",
2716
+ properties: {
2717
+ query: {
2718
+ type: "string",
2719
+ description: "Search query"
2720
+ }
2721
+ },
2722
+ required: ["query"]
2723
+ }
2724
+ }));
2725
+ }
2726
+
2495
2727
  // src/services/providers/gemini/GeminiChatService.ts
2496
2728
  var GeminiChatService = class {
2497
2729
  /**
@@ -2521,24 +2753,12 @@ If it's in another language, summarize in that language.
2521
2753
  this.tools = tools;
2522
2754
  this.mcpServers = mcpServers;
2523
2755
  }
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
2756
  isGemma4Model(model) {
2540
2757
  return /^gemma-4-/.test(model);
2541
2758
  }
2759
+ shouldMinimizeThinking(model) {
2760
+ return model === MODEL_GEMINI_3_5_FLASH || this.isGemma4Model(model);
2761
+ }
2542
2762
  shouldExposeTextPart(part, model) {
2543
2763
  if (!part.text) return false;
2544
2764
  if (this.isGemma4Model(model) && part.thought === true) {
@@ -2636,20 +2856,7 @@ If it's in another language, summarize in that language.
2636
2856
  this.mcpSchemasInitialized = true;
2637
2857
  } catch (error) {
2638
2858
  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
- }));
2859
+ this.mcpToolSchemas = createFallbackMCPToolSchemas(this.mcpServers);
2653
2860
  this.mcpSchemasInitialized = true;
2654
2861
  }
2655
2862
  }
@@ -2709,64 +2916,6 @@ If it's in another language, summarize in that language.
2709
2916
  }
2710
2917
  }
2711
2918
  /* ────────────────────────────────── */
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
2919
  /* HTTP call */
2771
2920
  /* ────────────────────────────────── */
2772
2921
  async callGemini(messages, model, stream = false, maxTokens) {
@@ -2775,52 +2924,38 @@ If it's in another language, summarize in that language.
2775
2924
  (b) => b?.type === "image_url" || b?.inlineData
2776
2925
  )
2777
2926
  );
2778
- const contents = hasVision ? await this.convertVisionMessagesToGeminiFormat(
2779
- messages
2780
- ) : this.convertMessagesToGeminiFormat(messages);
2927
+ const contents = hasVision ? await convertVisionMessagesToGeminiFormat(
2928
+ messages,
2929
+ {
2930
+ callIdMap: this.callIdMap
2931
+ }
2932
+ ) : convertMessagesToGeminiFormat(messages, {
2933
+ callIdMap: this.callIdMap
2934
+ });
2781
2935
  const body = {
2782
2936
  contents,
2783
2937
  generationConfig: {
2784
2938
  maxOutputTokens: maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength)
2785
2939
  }
2786
2940
  };
2787
- if (this.isGemma4Model(model)) {
2941
+ if (this.shouldMinimizeThinking(model)) {
2788
2942
  body.generationConfig.thinkingConfig = {
2789
2943
  includeThoughts: false,
2790
- thinkingLevel: "minimal"
2944
+ thinkingLevel: "MINIMAL"
2791
2945
  };
2792
2946
  }
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
- }
2947
+ let activeMCPToolSchemas = [];
2803
2948
  if (this.mcpServers.length > 0) {
2804
2949
  try {
2805
2950
  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
- );
2951
+ activeMCPToolSchemas = this.mcpToolSchemas;
2813
2952
  } catch (error) {
2814
2953
  console.warn("MCP initialization failed, skipping MCP tools:", error);
2815
2954
  }
2816
2955
  }
2817
- if (allToolDeclarations.length > 0) {
2818
- body.tools = [
2819
- {
2820
- functionDeclarations: allToolDeclarations
2821
- }
2822
- ];
2823
- body.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
2956
+ const toolConfig = buildGeminiToolConfig(this.tools, activeMCPToolSchemas);
2957
+ if (toolConfig) {
2958
+ Object.assign(body, toolConfig);
2824
2959
  }
2825
2960
  const fetchOnce = async (ver, payload) => {
2826
2961
  const fn = stream ? "streamGenerateContent" : "generateContent";
@@ -2831,8 +2966,8 @@ If it's in another language, summarize in that language.
2831
2966
  const isLite = /flash[-_]lite/.test(model);
2832
2967
  const isGemma4 = this.isGemma4Model(model);
2833
2968
  const isGemini25 = /gemini-2\.5/.test(model);
2834
- const isGemini3Preview = /^gemini-3(?:\.[0-9]+)?-.*preview/.test(model);
2835
- const requiresV1beta = isLite || isGemma4 || isGemini25 || isGemini3Preview;
2969
+ const isGemini3 = /^gemini-3(?:\.[0-9]+)?-/.test(model);
2970
+ const requiresV1beta = isLite || isGemma4 || isGemini25 || isGemini3;
2836
2971
  const firstVer = requiresV1beta ? "v1beta" : "v1";
2837
2972
  const tryApi = async () => {
2838
2973
  try {
@@ -2857,125 +2992,6 @@ If it's in another language, summarize in that language.
2857
2992
  throw error;
2858
2993
  }
2859
2994
  }
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
2995
  /* ────────────────────────────────────────────────────────── */
2980
2996
  /* Convert NDJSON stream to common format */
2981
2997
  /* ────────────────────────────────────────────────────────── */