nitrostack 1.0.1 → 1.0.2

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 (51) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cli/index.js +4 -1
  3. package/dist/cli/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/studio/README.md +140 -0
  6. package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
  7. package/src/studio/app/api/auth/register-client/route.ts +67 -0
  8. package/src/studio/app/api/chat/route.ts +123 -0
  9. package/src/studio/app/api/health/checks/route.ts +42 -0
  10. package/src/studio/app/api/health/route.ts +13 -0
  11. package/src/studio/app/api/init/route.ts +85 -0
  12. package/src/studio/app/api/ping/route.ts +13 -0
  13. package/src/studio/app/api/prompts/[name]/route.ts +21 -0
  14. package/src/studio/app/api/prompts/route.ts +13 -0
  15. package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
  16. package/src/studio/app/api/resources/route.ts +13 -0
  17. package/src/studio/app/api/roots/route.ts +13 -0
  18. package/src/studio/app/api/sampling/route.ts +14 -0
  19. package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
  20. package/src/studio/app/api/tools/route.ts +23 -0
  21. package/src/studio/app/api/widget-examples/route.ts +44 -0
  22. package/src/studio/app/auth/callback/page.tsx +160 -0
  23. package/src/studio/app/auth/page.tsx +543 -0
  24. package/src/studio/app/chat/page.tsx +530 -0
  25. package/src/studio/app/chat/page.tsx.backup +390 -0
  26. package/src/studio/app/globals.css +410 -0
  27. package/src/studio/app/health/page.tsx +177 -0
  28. package/src/studio/app/layout.tsx +48 -0
  29. package/src/studio/app/page.tsx +337 -0
  30. package/src/studio/app/page.tsx.backup +346 -0
  31. package/src/studio/app/ping/page.tsx +204 -0
  32. package/src/studio/app/prompts/page.tsx +228 -0
  33. package/src/studio/app/resources/page.tsx +313 -0
  34. package/src/studio/components/EnlargeModal.tsx +116 -0
  35. package/src/studio/components/Sidebar.tsx +133 -0
  36. package/src/studio/components/ToolCard.tsx +108 -0
  37. package/src/studio/components/WidgetRenderer.tsx +99 -0
  38. package/src/studio/lib/api.ts +207 -0
  39. package/src/studio/lib/llm-service.ts +361 -0
  40. package/src/studio/lib/mcp-client.ts +168 -0
  41. package/src/studio/lib/store.ts +192 -0
  42. package/src/studio/lib/theme-provider.tsx +50 -0
  43. package/src/studio/lib/types.ts +107 -0
  44. package/src/studio/lib/widget-loader.ts +90 -0
  45. package/src/studio/middleware.ts +27 -0
  46. package/src/studio/next.config.js +16 -0
  47. package/src/studio/package-lock.json +2696 -0
  48. package/src/studio/package.json +34 -0
  49. package/src/studio/postcss.config.mjs +10 -0
  50. package/src/studio/tailwind.config.ts +67 -0
  51. package/src/studio/tsconfig.json +41 -0
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { getWidgetUrl, createWidgetHTML, postMessageToWidget } from '@/lib/widget-loader';
5
+
6
+ interface WidgetRendererProps {
7
+ uri: string;
8
+ data: any;
9
+ className?: string;
10
+ }
11
+
12
+ export function WidgetRenderer({ uri, data, className = '' }: WidgetRendererProps) {
13
+ const iframeRef = useRef<HTMLIFrameElement>(null);
14
+
15
+ // Check if we're in dev mode (localhost)
16
+ const isDevMode =
17
+ typeof window !== 'undefined' &&
18
+ (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
19
+
20
+ useEffect(() => {
21
+ if (!iframeRef.current) return;
22
+
23
+ if (isDevMode) {
24
+ // Dev mode: load from widget dev server (port 3001)
25
+ // Widget URIs are like "/calculator-result" or "calculator-result"
26
+ // Remove leading slash if present
27
+ const widgetPath = uri.startsWith('/') ? uri.substring(1) : uri;
28
+ const widgetUrl = `http://localhost:3001/${widgetPath}`;
29
+
30
+ console.log('Loading widget in dev mode:', { uri, widgetPath, widgetUrl, data });
31
+
32
+ // Set up onload handler BEFORE setting src
33
+ iframeRef.current.onload = () => {
34
+ console.log('Widget iframe loaded, posting data...');
35
+ // Post message after a short delay to ensure widget is ready
36
+ setTimeout(() => {
37
+ try {
38
+ iframeRef.current?.contentWindow?.postMessage(
39
+ {
40
+ type: 'toolOutput',
41
+ data,
42
+ },
43
+ '*'
44
+ );
45
+ console.log('✅ Data posted to widget:', data);
46
+ } catch (e) {
47
+ console.error('❌ Failed to post message to widget:', e);
48
+ }
49
+ }, 300);
50
+ };
51
+
52
+ // Set src AFTER onload handler is set
53
+ iframeRef.current.src = widgetUrl;
54
+ } else {
55
+ // Production mode: fetch and render
56
+ const loadProductionWidget = async () => {
57
+ try {
58
+ const response = await fetch(`/api/resources/${encodeURIComponent(uri)}`);
59
+ const result = await response.json();
60
+
61
+ if (result.contents && result.contents.length > 0) {
62
+ const html = result.contents[0].text || '';
63
+ const completeHtml = createWidgetHTML(html, data);
64
+
65
+ // Use Blob URL
66
+ const blob = new Blob([completeHtml], { type: 'text/html' });
67
+ const blobUrl = URL.createObjectURL(blob);
68
+
69
+ if (iframeRef.current) {
70
+ iframeRef.current.src = blobUrl;
71
+ iframeRef.current.onload = () => {
72
+ URL.revokeObjectURL(blobUrl);
73
+ };
74
+ }
75
+ }
76
+ } catch (error) {
77
+ console.error('Failed to load widget:', error);
78
+ }
79
+ };
80
+
81
+ loadProductionWidget();
82
+ }
83
+ }, [uri, data, isDevMode]);
84
+
85
+ return (
86
+ <iframe
87
+ ref={iframeRef}
88
+ className={className}
89
+ sandbox="allow-scripts allow-same-origin"
90
+ style={{
91
+ width: '100%',
92
+ height: '100%',
93
+ border: 'none',
94
+ background: 'transparent',
95
+ }}
96
+ />
97
+ );
98
+ }
99
+
@@ -0,0 +1,207 @@
1
+ // NitroStack Studio API Client
2
+
3
+ const API_BASE = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000';
4
+
5
+ export class StudioAPI {
6
+ private baseUrl: string;
7
+ private initialized: boolean = false;
8
+
9
+ constructor(baseUrl: string = API_BASE) {
10
+ this.baseUrl = baseUrl;
11
+ }
12
+
13
+ // Initialize MCP connection (call once on app start)
14
+ async initialize() {
15
+ if (this.initialized) return;
16
+ try {
17
+ await fetch(`${this.baseUrl}/api/init`, { method: 'POST' });
18
+ this.initialized = true;
19
+ } catch (error) {
20
+ console.error('Failed to initialize MCP connection:', error);
21
+ }
22
+ }
23
+
24
+ // Connection
25
+ async checkConnection() {
26
+ const response = await fetch(`${this.baseUrl}/api/health`);
27
+ return response.json();
28
+ }
29
+
30
+ // Tools
31
+ async listTools() {
32
+ const response = await fetch(`${this.baseUrl}/api/tools`);
33
+ return response.json();
34
+ }
35
+
36
+ async getTools() {
37
+ return this.listTools();
38
+ }
39
+
40
+ async callTool(name: string, args: any, jwtToken?: string, apiKey?: string) {
41
+ const response = await fetch(`${this.baseUrl}/api/tools/${name}/call`, {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify({ args, jwtToken, apiKey }),
45
+ });
46
+ return response.json();
47
+ }
48
+
49
+ // Resources
50
+ async getResources() {
51
+ const response = await fetch(`${this.baseUrl}/api/resources`);
52
+ return response.json();
53
+ }
54
+
55
+ async getResource(uri: string) {
56
+ const response = await fetch(`${this.baseUrl}/api/resources/${encodeURIComponent(uri)}`);
57
+ return response.json();
58
+ }
59
+
60
+ // Prompts
61
+ async getPrompts() {
62
+ const response = await fetch(`${this.baseUrl}/api/prompts`);
63
+ return response.json();
64
+ }
65
+
66
+ async executePrompt(name: string, args: any) {
67
+ const response = await fetch(`${this.baseUrl}/api/prompts/${name}`, {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify(args),
71
+ });
72
+ return response.json();
73
+ }
74
+
75
+ // Ping
76
+ async ping() {
77
+ const response = await fetch(`${this.baseUrl}/api/ping`);
78
+ return response.json();
79
+ }
80
+
81
+ // Sampling
82
+ async sample(params: { prompt: string; maxTokens: number; model?: string }) {
83
+ const response = await fetch(`${this.baseUrl}/api/sampling`, {
84
+ method: 'POST',
85
+ headers: { 'Content-Type': 'application/json' },
86
+ body: JSON.stringify(params),
87
+ });
88
+ return response.json();
89
+ }
90
+
91
+ // Roots
92
+ async getRoots() {
93
+ const response = await fetch(`${this.baseUrl}/api/roots`);
94
+ return response.json();
95
+ }
96
+
97
+ // Chat
98
+ async chat(params: {
99
+ provider: string;
100
+ messages: any[];
101
+ apiKey: string; // LLM API key (OpenAI/Gemini)
102
+ jwtToken?: string; // MCP server JWT token
103
+ mcpApiKey?: string; // MCP server API key
104
+ }) {
105
+ const response = await fetch(`${this.baseUrl}/api/chat`, {
106
+ method: 'POST',
107
+ headers: { 'Content-Type': 'application/json' },
108
+ body: JSON.stringify(params),
109
+ });
110
+ return response.json();
111
+ }
112
+
113
+ // Auth (OAuth 2.1)
114
+ async discoverAuth(url: string, type: 'resource' | 'auth-server') {
115
+ const response = await fetch(`${this.baseUrl}/api/auth/fetch-metadata`, {
116
+ method: 'POST',
117
+ headers: { 'Content-Type': 'application/json' },
118
+ body: JSON.stringify({ url, type }),
119
+ });
120
+ return response.json();
121
+ }
122
+
123
+ async registerClient(endpoint: string, metadata: any) {
124
+ const response = await fetch(`${this.baseUrl}/api/auth/register-client`, {
125
+ method: 'POST',
126
+ headers: { 'Content-Type': 'application/json' },
127
+ body: JSON.stringify({ endpoint, metadata }),
128
+ });
129
+ return response.json();
130
+ }
131
+
132
+ async startOAuthFlow(params: {
133
+ authorizationEndpoint: string;
134
+ clientId: string;
135
+ redirectUri: string;
136
+ scope: string;
137
+ resource: string;
138
+ }) {
139
+ const response = await fetch(`${this.baseUrl}/api/auth/start-flow`, {
140
+ method: 'POST',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify(params),
143
+ });
144
+ return response.json();
145
+ }
146
+
147
+ async exchangeToken(params: {
148
+ code: string;
149
+ pkce: any;
150
+ tokenEndpoint: string;
151
+ clientId: string;
152
+ clientSecret?: string;
153
+ redirectUri: string;
154
+ resource: string;
155
+ }) {
156
+ const response = await fetch(`${this.baseUrl}/api/auth/exchange-token`, {
157
+ method: 'POST',
158
+ headers: { 'Content-Type': 'application/json' },
159
+ body: JSON.stringify(params),
160
+ });
161
+ return response.json();
162
+ }
163
+
164
+ async refreshToken(params: {
165
+ refreshToken: string;
166
+ tokenEndpoint: string;
167
+ clientId: string;
168
+ clientSecret?: string;
169
+ resource: string;
170
+ }) {
171
+ const response = await fetch(`${this.baseUrl}/api/auth/refresh-token`, {
172
+ method: 'POST',
173
+ headers: { 'Content-Type': 'application/json' },
174
+ body: JSON.stringify(params),
175
+ });
176
+ return response.json();
177
+ }
178
+
179
+ async revokeToken(params: {
180
+ token: string;
181
+ revocationEndpoint: string;
182
+ clientId: string;
183
+ clientSecret?: string;
184
+ }) {
185
+ const response = await fetch(`${this.baseUrl}/api/auth/revoke-token`, {
186
+ method: 'POST',
187
+ headers: { 'Content-Type': 'application/json' },
188
+ body: JSON.stringify(params),
189
+ });
190
+ return response.json();
191
+ }
192
+
193
+ // Health
194
+ async getHealth() {
195
+ const response = await fetch(`${this.baseUrl}/api/health/checks`);
196
+ return response.json();
197
+ }
198
+
199
+ // Widget Examples
200
+ async getWidgetExamples() {
201
+ const response = await fetch(`${this.baseUrl}/api/widget-examples`);
202
+ return response.json();
203
+ }
204
+ }
205
+
206
+ export const api = new StudioAPI();
207
+
@@ -0,0 +1,361 @@
1
+ // LLM Service for Studio
2
+ // Supports OpenAI and Gemini
3
+
4
+ export type LLMProvider = 'openai' | 'gemini';
5
+
6
+ export interface ChatMessage {
7
+ role: 'user' | 'assistant' | 'tool' | 'system';
8
+ content: string;
9
+ toolCalls?: ToolCall[];
10
+ toolCallId?: string; // For tool responses - the ID of the call being responded to
11
+ toolName?: string; // For tool responses - the name of the tool (required by Gemini)
12
+ }
13
+
14
+ export interface ToolCall {
15
+ id: string;
16
+ name: string;
17
+ arguments: any;
18
+ }
19
+
20
+ export interface ChatResponse {
21
+ message: ChatMessage;
22
+ toolCalls?: ToolCall[];
23
+ finishReason?: string;
24
+ }
25
+
26
+ export class LLMService {
27
+ async chat(
28
+ provider: LLMProvider,
29
+ messages: ChatMessage[],
30
+ tools: any[],
31
+ apiKey: string
32
+ ): Promise<ChatResponse> {
33
+ if (provider === 'openai') {
34
+ return this.chatOpenAI(messages, tools, apiKey);
35
+ } else if (provider === 'gemini') {
36
+ return this.chatGemini(messages, tools, apiKey);
37
+ }
38
+ throw new Error(`Unsupported provider: ${provider}`);
39
+ }
40
+
41
+ private async chatOpenAI(
42
+ messages: ChatMessage[],
43
+ tools: any[],
44
+ apiKey: string
45
+ ): Promise<ChatResponse> {
46
+ const formattedMessages = messages.map((msg) => {
47
+ if (msg.role === 'tool') {
48
+ return {
49
+ role: 'tool' as const,
50
+ content: msg.content,
51
+ tool_call_id: msg.toolCallId,
52
+ };
53
+ }
54
+
55
+ // Handle assistant messages with tool calls
56
+ if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
57
+ return {
58
+ role: 'assistant' as const,
59
+ content: msg.content || null,
60
+ tool_calls: msg.toolCalls.map(tc => ({
61
+ id: tc.id,
62
+ type: 'function' as const,
63
+ function: {
64
+ name: tc.name,
65
+ arguments: typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments),
66
+ },
67
+ })),
68
+ };
69
+ }
70
+
71
+ return {
72
+ role: msg.role,
73
+ content: msg.content,
74
+ };
75
+ });
76
+
77
+ console.log('Formatted messages for OpenAI:', JSON.stringify(formattedMessages, null, 2));
78
+
79
+ const requestBody: any = {
80
+ model: 'gpt-4-turbo-preview',
81
+ messages: formattedMessages,
82
+ };
83
+
84
+ if (tools.length > 0) {
85
+ requestBody.tools = tools.map((tool) => {
86
+ // Clean the schema for OpenAI - remove unsupported properties
87
+ const cleanSchema = { ...tool.inputSchema };
88
+
89
+ // Remove properties that OpenAI doesn't support
90
+ delete cleanSchema.$schema;
91
+ delete cleanSchema.additionalProperties;
92
+
93
+ // Ensure required properties for OpenAI
94
+ if (!cleanSchema.type) {
95
+ cleanSchema.type = 'object';
96
+ }
97
+ if (!cleanSchema.properties) {
98
+ cleanSchema.properties = {};
99
+ }
100
+
101
+ return {
102
+ type: 'function',
103
+ function: {
104
+ name: tool.name,
105
+ description: tool.description || 'No description provided',
106
+ parameters: cleanSchema,
107
+ },
108
+ };
109
+ });
110
+
111
+ console.log('Sending tools to OpenAI:', JSON.stringify(requestBody.tools, null, 2));
112
+ }
113
+
114
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
115
+ method: 'POST',
116
+ headers: {
117
+ 'Content-Type': 'application/json',
118
+ Authorization: `Bearer ${apiKey}`,
119
+ },
120
+ body: JSON.stringify(requestBody),
121
+ });
122
+
123
+ if (!response.ok) {
124
+ const errorData = await response.json().catch(() => ({}));
125
+ console.error('OpenAI API error details:', errorData);
126
+ throw new Error(`OpenAI API error: ${response.statusText} - ${JSON.stringify(errorData)}`);
127
+ }
128
+
129
+ const data = await response.json();
130
+ const choice = data.choices[0];
131
+
132
+ const result: ChatResponse = {
133
+ message: {
134
+ role: 'assistant',
135
+ content: choice.message.content || '',
136
+ },
137
+ finishReason: choice.finish_reason,
138
+ };
139
+
140
+ if (choice.message.tool_calls) {
141
+ result.toolCalls = choice.message.tool_calls.map((tc: any) => ({
142
+ id: tc.id,
143
+ name: tc.function?.name || '',
144
+ arguments: JSON.parse(tc.function?.arguments || '{}'),
145
+ }));
146
+ result.message.toolCalls = result.toolCalls;
147
+ }
148
+
149
+ return result;
150
+ }
151
+
152
+ private async chatGemini(
153
+ messages: ChatMessage[],
154
+ tools: any[],
155
+ apiKey: string
156
+ ): Promise<ChatResponse> {
157
+ // Convert messages to Gemini format
158
+ const contents: any[] = [];
159
+ let systemInstruction = '';
160
+
161
+ // Group consecutive tool messages together for Gemini
162
+ let i = 0;
163
+ while (i < messages.length) {
164
+ const msg = messages[i];
165
+
166
+ if (msg.role === 'system') {
167
+ systemInstruction = msg.content;
168
+ i++;
169
+ continue;
170
+ }
171
+
172
+ if (msg.role === 'tool') {
173
+ // Collect all consecutive tool messages and group them into ONE function response
174
+ const functionParts: any[] = [];
175
+
176
+ while (i < messages.length && messages[i].role === 'tool') {
177
+ const toolMsg = messages[i];
178
+ functionParts.push({
179
+ functionResponse: {
180
+ name: toolMsg.toolName || 'unknown', // Use toolName for Gemini!
181
+ response: {
182
+ content: toolMsg.content,
183
+ },
184
+ },
185
+ });
186
+ i++;
187
+ }
188
+
189
+ // Add all function responses as ONE entry with multiple parts
190
+ contents.push({
191
+ role: 'function',
192
+ parts: functionParts,
193
+ });
194
+ } else if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
195
+ // Assistant message with tool calls
196
+ const parts: any[] = [];
197
+
198
+ if (msg.content) {
199
+ parts.push({ text: msg.content });
200
+ }
201
+
202
+ for (const tc of msg.toolCalls) {
203
+ parts.push({
204
+ functionCall: {
205
+ name: tc.name,
206
+ args: tc.arguments,
207
+ },
208
+ });
209
+ }
210
+
211
+ contents.push({
212
+ role: 'model',
213
+ parts,
214
+ });
215
+ i++;
216
+ } else {
217
+ // Regular user or assistant message
218
+ contents.push({
219
+ role: msg.role === 'assistant' ? 'model' : 'user',
220
+ parts: [{ text: msg.content }],
221
+ });
222
+ i++;
223
+ }
224
+ }
225
+
226
+ console.log('Formatted contents for Gemini:', JSON.stringify(contents, null, 2));
227
+
228
+ // Prepare request body
229
+ const requestBody: any = {
230
+ contents,
231
+ };
232
+
233
+ // Add system instruction if present
234
+ if (systemInstruction) {
235
+ requestBody.systemInstruction = {
236
+ parts: [{ text: systemInstruction }],
237
+ };
238
+ }
239
+
240
+ // Add tools if present
241
+ if (tools.length > 0) {
242
+ requestBody.tools = [{
243
+ functionDeclarations: tools.map((tool) => {
244
+ const cleanSchema = { ...tool.inputSchema };
245
+
246
+ // Remove unsupported properties
247
+ delete cleanSchema.$schema;
248
+ delete cleanSchema.additionalProperties;
249
+
250
+ // Convert to Gemini's expected format
251
+ const parameters: any = {
252
+ type: cleanSchema.type || 'OBJECT',
253
+ properties: {},
254
+ required: cleanSchema.required || [],
255
+ };
256
+
257
+ // Convert properties
258
+ if (cleanSchema.properties) {
259
+ for (const [key, value] of Object.entries(cleanSchema.properties)) {
260
+ const prop: any = value;
261
+ parameters.properties[key] = {
262
+ type: this.convertTypeToGemini(prop.type),
263
+ description: prop.description || '',
264
+ };
265
+
266
+ // Handle enums
267
+ if (prop.enum) {
268
+ parameters.properties[key].enum = prop.enum;
269
+ }
270
+ }
271
+ }
272
+
273
+ return {
274
+ name: tool.name,
275
+ description: tool.description || 'No description provided',
276
+ parameters,
277
+ };
278
+ }),
279
+ }];
280
+
281
+ console.log('Sending tools to Gemini:', JSON.stringify(requestBody.tools, null, 2));
282
+ }
283
+
284
+ // Use Gemini 2.0 Flash Experimental (latest model with function calling)
285
+ // The v1beta API uses 'gemini-2.0-flash-exp' for the newest features
286
+ const model = 'gemini-2.0-flash-exp';
287
+ const response = await fetch(
288
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`,
289
+ {
290
+ method: 'POST',
291
+ headers: { 'Content-Type': 'application/json' },
292
+ body: JSON.stringify(requestBody),
293
+ }
294
+ );
295
+
296
+ if (!response.ok) {
297
+ const errorData = await response.json().catch(() => ({}));
298
+ console.error('Gemini API error details:', errorData);
299
+ throw new Error(`Gemini API error: ${response.statusText} - ${JSON.stringify(errorData)}`);
300
+ }
301
+
302
+ const data = await response.json();
303
+ console.log('Gemini response:', JSON.stringify(data, null, 2));
304
+
305
+ const candidate = data.candidates?.[0];
306
+ if (!candidate) {
307
+ throw new Error('No response from Gemini');
308
+ }
309
+
310
+ const parts = candidate.content?.parts || [];
311
+
312
+ // Extract text content
313
+ let content = '';
314
+ const toolCalls: ToolCall[] = [];
315
+
316
+ for (const part of parts) {
317
+ if (part.text) {
318
+ content += part.text;
319
+ }
320
+
321
+ if (part.functionCall) {
322
+ toolCalls.push({
323
+ id: `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
324
+ name: part.functionCall.name,
325
+ arguments: part.functionCall.args || {},
326
+ });
327
+ }
328
+ }
329
+
330
+ const result: ChatResponse = {
331
+ message: {
332
+ role: 'assistant',
333
+ content: content || '',
334
+ },
335
+ finishReason: candidate.finishReason,
336
+ };
337
+
338
+ if (toolCalls.length > 0) {
339
+ result.toolCalls = toolCalls;
340
+ result.message.toolCalls = toolCalls;
341
+ }
342
+
343
+ return result;
344
+ }
345
+
346
+ private convertTypeToGemini(type?: string): string {
347
+ if (!type) return 'STRING';
348
+
349
+ const typeMap: Record<string, string> = {
350
+ 'string': 'STRING',
351
+ 'number': 'NUMBER',
352
+ 'integer': 'INTEGER',
353
+ 'boolean': 'BOOLEAN',
354
+ 'array': 'ARRAY',
355
+ 'object': 'OBJECT',
356
+ };
357
+
358
+ return typeMap[type.toLowerCase()] || 'STRING';
359
+ }
360
+ }
361
+