@aituber-onair/chat 0.35.0 → 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 (83) hide show
  1. package/README.ja.md +2 -2
  2. package/README.md +2 -2
  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/services/providers/claude/ClaudeChatService.d.ts +0 -24
  8. package/dist/cjs/services/providers/claude/ClaudeChatService.d.ts.map +1 -1
  9. package/dist/cjs/services/providers/claude/ClaudeChatService.js +3 -113
  10. package/dist/cjs/services/providers/claude/ClaudeChatService.js.map +1 -1
  11. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.d.ts.map +1 -1
  12. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.js +1 -0
  13. package/dist/cjs/services/providers/claude/ClaudeChatServiceProvider.js.map +1 -1
  14. package/dist/cjs/services/providers/claude/claudeMessageConverter.d.ts +6 -0
  15. package/dist/cjs/services/providers/claude/claudeMessageConverter.d.ts.map +1 -0
  16. package/dist/cjs/services/providers/claude/claudeMessageConverter.js +95 -0
  17. package/dist/cjs/services/providers/claude/claudeMessageConverter.js.map +1 -0
  18. package/dist/cjs/services/providers/gemini/GeminiChatService.d.ts +0 -21
  19. package/dist/cjs/services/providers/gemini/GeminiChatService.d.ts.map +1 -1
  20. package/dist/cjs/services/providers/gemini/GeminiChatService.js +14 -254
  21. package/dist/cjs/services/providers/gemini/GeminiChatService.js.map +1 -1
  22. package/dist/cjs/services/providers/gemini/geminiMessageConverter.d.ts +17 -0
  23. package/dist/cjs/services/providers/gemini/geminiMessageConverter.d.ts.map +1 -0
  24. package/dist/cjs/services/providers/gemini/geminiMessageConverter.js +183 -0
  25. package/dist/cjs/services/providers/gemini/geminiMessageConverter.js.map +1 -0
  26. package/dist/cjs/services/providers/gemini/geminiToolAdapter.d.ts +23 -0
  27. package/dist/cjs/services/providers/gemini/geminiToolAdapter.d.ts.map +1 -0
  28. package/dist/cjs/services/providers/gemini/geminiToolAdapter.js +47 -0
  29. package/dist/cjs/services/providers/gemini/geminiToolAdapter.js.map +1 -0
  30. package/dist/cjs/services/providers/openai/OpenAIChatService.d.ts +3 -28
  31. package/dist/cjs/services/providers/openai/OpenAIChatService.d.ts.map +1 -1
  32. package/dist/cjs/services/providers/openai/OpenAIChatService.js +15 -250
  33. package/dist/cjs/services/providers/openai/OpenAIChatService.js.map +1 -1
  34. package/dist/cjs/services/providers/openai/openaiRequestBuilder.d.ts +33 -0
  35. package/dist/cjs/services/providers/openai/openaiRequestBuilder.d.ts.map +1 -0
  36. package/dist/cjs/services/providers/openai/openaiRequestBuilder.js +218 -0
  37. package/dist/cjs/services/providers/openai/openaiRequestBuilder.js.map +1 -0
  38. package/dist/cjs/services/providers/openai/openaiToolBuilder.d.ts +9 -0
  39. package/dist/cjs/services/providers/openai/openaiToolBuilder.d.ts.map +1 -0
  40. package/dist/cjs/services/providers/openai/openaiToolBuilder.js +36 -0
  41. package/dist/cjs/services/providers/openai/openaiToolBuilder.js.map +1 -0
  42. package/dist/esm/constants/claude.d.ts +1 -0
  43. package/dist/esm/constants/claude.d.ts.map +1 -1
  44. package/dist/esm/constants/claude.js +2 -0
  45. package/dist/esm/constants/claude.js.map +1 -1
  46. package/dist/esm/services/providers/claude/ClaudeChatService.d.ts +0 -24
  47. package/dist/esm/services/providers/claude/ClaudeChatService.d.ts.map +1 -1
  48. package/dist/esm/services/providers/claude/ClaudeChatService.js +3 -113
  49. package/dist/esm/services/providers/claude/ClaudeChatService.js.map +1 -1
  50. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.d.ts.map +1 -1
  51. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.js +2 -1
  52. package/dist/esm/services/providers/claude/ClaudeChatServiceProvider.js.map +1 -1
  53. package/dist/esm/services/providers/claude/claudeMessageConverter.d.ts +6 -0
  54. package/dist/esm/services/providers/claude/claudeMessageConverter.d.ts.map +1 -0
  55. package/dist/esm/services/providers/claude/claudeMessageConverter.js +89 -0
  56. package/dist/esm/services/providers/claude/claudeMessageConverter.js.map +1 -0
  57. package/dist/esm/services/providers/gemini/GeminiChatService.d.ts +0 -21
  58. package/dist/esm/services/providers/gemini/GeminiChatService.d.ts.map +1 -1
  59. package/dist/esm/services/providers/gemini/GeminiChatService.js +14 -254
  60. package/dist/esm/services/providers/gemini/GeminiChatService.js.map +1 -1
  61. package/dist/esm/services/providers/gemini/geminiMessageConverter.d.ts +17 -0
  62. package/dist/esm/services/providers/gemini/geminiMessageConverter.d.ts.map +1 -0
  63. package/dist/esm/services/providers/gemini/geminiMessageConverter.js +178 -0
  64. package/dist/esm/services/providers/gemini/geminiMessageConverter.js.map +1 -0
  65. package/dist/esm/services/providers/gemini/geminiToolAdapter.d.ts +23 -0
  66. package/dist/esm/services/providers/gemini/geminiToolAdapter.d.ts.map +1 -0
  67. package/dist/esm/services/providers/gemini/geminiToolAdapter.js +42 -0
  68. package/dist/esm/services/providers/gemini/geminiToolAdapter.js.map +1 -0
  69. package/dist/esm/services/providers/openai/OpenAIChatService.d.ts +3 -28
  70. package/dist/esm/services/providers/openai/OpenAIChatService.d.ts.map +1 -1
  71. package/dist/esm/services/providers/openai/OpenAIChatService.js +17 -252
  72. package/dist/esm/services/providers/openai/OpenAIChatService.js.map +1 -1
  73. package/dist/esm/services/providers/openai/openaiRequestBuilder.d.ts +33 -0
  74. package/dist/esm/services/providers/openai/openaiRequestBuilder.d.ts.map +1 -0
  75. package/dist/esm/services/providers/openai/openaiRequestBuilder.js +212 -0
  76. package/dist/esm/services/providers/openai/openaiRequestBuilder.js.map +1 -0
  77. package/dist/esm/services/providers/openai/openaiToolBuilder.d.ts +9 -0
  78. package/dist/esm/services/providers/openai/openaiToolBuilder.d.ts.map +1 -0
  79. package/dist/esm/services/providers/openai/openaiToolBuilder.js +33 -0
  80. package/dist/esm/services/providers/openai/openaiToolBuilder.js.map +1 -0
  81. package/dist/umd/aituber-onair-chat.js +551 -541
  82. package/dist/umd/aituber-onair-chat.min.js +10 -10
  83. package/package.json +1 -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,
@@ -346,6 +347,7 @@ var AITuberOnAirChat = (() => {
346
347
  var MODEL_CLAUDE_4_6_SONNET = "claude-sonnet-4-6";
347
348
  var MODEL_CLAUDE_4_6_OPUS = "claude-opus-4-6";
348
349
  var MODEL_CLAUDE_4_7_OPUS = "claude-opus-4-7";
350
+ var MODEL_CLAUDE_4_8_OPUS = "claude-opus-4-8";
349
351
  var CLAUDE_VISION_SUPPORTED_MODELS = [
350
352
  MODEL_CLAUDE_3_HAIKU,
351
353
  MODEL_CLAUDE_4_SONNET,
@@ -355,7 +357,8 @@ var AITuberOnAirChat = (() => {
355
357
  MODEL_CLAUDE_4_5_OPUS,
356
358
  MODEL_CLAUDE_4_6_SONNET,
357
359
  MODEL_CLAUDE_4_6_OPUS,
358
- MODEL_CLAUDE_4_7_OPUS
360
+ MODEL_CLAUDE_4_7_OPUS,
361
+ MODEL_CLAUDE_4_8_OPUS
359
362
  ];
360
363
 
361
364
  // src/constants/openrouter.ts
@@ -1247,6 +1250,94 @@ If it's in another language, summarize in that language.
1247
1250
  };
1248
1251
  }
1249
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
+
1250
1341
  // src/services/providers/claude/ClaudeChatService.ts
1251
1342
  var ClaudeChatService = class {
1252
1343
  /**
@@ -1353,112 +1444,6 @@ If it's in another language, summarize in that language.
1353
1444
  toolErrorMessage: "processVisionChat received tool_calls. ChatProcessor must use chatOnce() loop when tools are enabled."
1354
1445
  });
1355
1446
  }
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
1447
  /**
1463
1448
  * Call Claude API
1464
1449
  * @param messages Array of messages to send
@@ -1478,9 +1463,7 @@ If it's in another language, summarize in that language.
1478
1463
  const body = {
1479
1464
  model,
1480
1465
  system,
1481
- messages: hasVision ? this.convertVisionMessagesToClaudeFormat(
1482
- content
1483
- ) : this.convertMessagesToClaudeFormat(content),
1466
+ messages: hasVision ? convertVisionMessagesToClaudeFormat(content) : convertMessagesToClaudeFormat(content),
1484
1467
  stream,
1485
1468
  max_tokens: maxTokens !== void 0 ? maxTokens : getMaxTokensForResponseLength(this.responseLength)
1486
1469
  };
@@ -1743,6 +1726,7 @@ If it's in another language, summarize in that language.
1743
1726
  MODEL_CLAUDE_4_6_SONNET,
1744
1727
  MODEL_CLAUDE_4_6_OPUS,
1745
1728
  MODEL_CLAUDE_4_7_OPUS,
1729
+ MODEL_CLAUDE_4_8_OPUS,
1746
1730
  MODEL_CLAUDE_3_HAIKU
1747
1731
  ];
1748
1732
  }
@@ -1934,7 +1918,49 @@ If it's in another language, summarize in that language.
1934
1918
  };
1935
1919
  }
1936
1920
 
1937
- // 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
1938
1964
  var GPT5_RESPONSE_LENGTH_MIN_TOKENS = {
1939
1965
  [CHAT_RESPONSE_LENGTH.VERY_SHORT]: 800,
1940
1966
  [CHAT_RESPONSE_LENGTH.SHORT]: 1200,
@@ -1956,6 +1982,177 @@ If it's in another language, summarize in that language.
1956
1982
  "deepseek",
1957
1983
  "mistral"
1958
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
1959
2156
  var OpenAIChatService = class {
1960
2157
  /**
1961
2158
  * Constructor
@@ -2108,7 +2305,20 @@ If it's in another language, summarize in that language.
2108
2305
  return stream ? this.parseStream(res, onPartialResponse) : this.parseOneShot(await res.json());
2109
2306
  }
2110
2307
  async callOpenAI(messages, model, stream = false, maxTokens) {
2111
- 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
+ });
2112
2322
  const headers = {};
2113
2323
  const shouldSendAuthorization = this.provider !== "openai-compatible" || this.apiKey.trim() !== "";
2114
2324
  if (shouldSendAuthorization) {
@@ -2117,199 +2327,6 @@ If it's in another language, summarize in that language.
2117
2327
  const res = await ChatServiceHttpClient.post(this.endpoint, body, headers);
2118
2328
  return res;
2119
2329
  }
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
2330
  async handleStream(res, onPartial) {
2314
2331
  return parseOpenAICompatibleTextStream(res, onPartial);
2315
2332
  }
@@ -2321,9 +2338,6 @@ If it's in another language, summarize in that language.
2321
2338
  parseOneShot(data) {
2322
2339
  return parseOpenAICompatibleOneShot(data);
2323
2340
  }
2324
- usesCompatibleChatCompletions() {
2325
- return OPENAI_COMPATIBLE_CHAT_COMPLETIONS_PROVIDERS.has(this.provider);
2326
- }
2327
2341
  };
2328
2342
 
2329
2343
  // src/services/providers/deepseek/DeepSeekChatService.ts
@@ -2495,6 +2509,221 @@ If it's in another language, summarize in that language.
2495
2509
  }
2496
2510
  };
2497
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
+
2498
2727
  // src/services/providers/gemini/GeminiChatService.ts
2499
2728
  var GeminiChatService = class {
2500
2729
  /**
@@ -2524,21 +2753,6 @@ If it's in another language, summarize in that language.
2524
2753
  this.tools = tools;
2525
2754
  this.mcpServers = mcpServers;
2526
2755
  }
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
2756
  isGemma4Model(model) {
2543
2757
  return /^gemma-4-/.test(model);
2544
2758
  }
@@ -2642,20 +2856,7 @@ If it's in another language, summarize in that language.
2642
2856
  this.mcpSchemasInitialized = true;
2643
2857
  } catch (error) {
2644
2858
  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
- }));
2859
+ this.mcpToolSchemas = createFallbackMCPToolSchemas(this.mcpServers);
2659
2860
  this.mcpSchemasInitialized = true;
2660
2861
  }
2661
2862
  }
@@ -2715,64 +2916,6 @@ If it's in another language, summarize in that language.
2715
2916
  }
2716
2917
  }
2717
2918
  /* ────────────────────────────────── */
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
2919
  /* HTTP call */
2777
2920
  /* ────────────────────────────────── */
2778
2921
  async callGemini(messages, model, stream = false, maxTokens) {
@@ -2781,9 +2924,14 @@ If it's in another language, summarize in that language.
2781
2924
  (b) => b?.type === "image_url" || b?.inlineData
2782
2925
  )
2783
2926
  );
2784
- const contents = hasVision ? await this.convertVisionMessagesToGeminiFormat(
2785
- messages
2786
- ) : 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
+ });
2787
2935
  const body = {
2788
2936
  contents,
2789
2937
  generationConfig: {
@@ -2796,37 +2944,18 @@ If it's in another language, summarize in that language.
2796
2944
  thinkingLevel: "MINIMAL"
2797
2945
  };
2798
2946
  }
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
- }
2947
+ let activeMCPToolSchemas = [];
2809
2948
  if (this.mcpServers.length > 0) {
2810
2949
  try {
2811
2950
  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
- );
2951
+ activeMCPToolSchemas = this.mcpToolSchemas;
2819
2952
  } catch (error) {
2820
2953
  console.warn("MCP initialization failed, skipping MCP tools:", error);
2821
2954
  }
2822
2955
  }
2823
- if (allToolDeclarations.length > 0) {
2824
- body.tools = [
2825
- {
2826
- functionDeclarations: allToolDeclarations
2827
- }
2828
- ];
2829
- body.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
2956
+ const toolConfig = buildGeminiToolConfig(this.tools, activeMCPToolSchemas);
2957
+ if (toolConfig) {
2958
+ Object.assign(body, toolConfig);
2830
2959
  }
2831
2960
  const fetchOnce = async (ver, payload) => {
2832
2961
  const fn = stream ? "streamGenerateContent" : "generateContent";
@@ -2863,125 +2992,6 @@ If it's in another language, summarize in that language.
2863
2992
  throw error;
2864
2993
  }
2865
2994
  }
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
2995
  /* ────────────────────────────────────────────────────────── */
2986
2996
  /* Convert NDJSON stream to common format */
2987
2997
  /* ────────────────────────────────────────────────────────── */