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