Files

1 line
24 KiB
JSON

{"repo": "bitrix24/b24jssdk", "files": {"docs/server/api/ai.post.ts": {"32c5256ff0e3b13a1d3c44abd05677d9b7b3fdd2": {"t": 1785745568.1800275, "n": 8508, "h": "62cdc6d2cc732c74", "c": "// @memo we not use jsonSchema\nimport { streamText, convertToModelMessages, smoothStream, stepCountIs, jsonSchema } from 'ai'\nimport { getHeader } from 'h3'\nimport { createDeepSeek } from '@ai-sdk/deepseek'\nimport { z } from 'zod'\nimport { tools as mcpToolDefinitions } from '#nuxt-mcp-toolkit/tools.mjs'\n\n/**\n * @docs https://ai-sdk.dev/providers/ai-sdk-providers/deepseek\n * @memo sync with: docs/content/docs/2.components/chat.md:297\n */\n\nconst maxStepCount = 5\n\n// async function initMcpToolsForUI() {\n// const httpTransport = new StreamableHTTPClientTransport(\n// new URL(`https://b24ui.bx-shef.by/b24ui/mcp/`)\n// )\n// const httpClient = await experimental_createMCPClient({\n// transport: httpTransport\n// })\n// return await httpClient.tools()\n// }\n//\n// async function initMcpToolsForB24RestApi() {\n// const httpTransport = new StreamableHTTPClientTransport(\n// new URL(`https://mcp-dev.bitrix24.tech/mcp`)\n// )\n// const httpClient = await experimental_createMCPClient({\n// transport: httpTransport\n// })\n// return await httpClient.tools()\n// }\n\nfunction mcpToolsToAiTools() {\n const aiTools: Record<string, { description: string, inputSchema: ReturnType<typeof jsonSchema>, execute: (args: any) => Promise<any> }> = {}\n\n for (const def of mcpToolDefinitions as any[]) {\n const filename = def._meta?.filename as string | undefined\n const name = def.name || (filename\n ? filename.replace(/\\.(ts|js|mts|mjs)$/, '').replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/[_\\s]+/g, '-').toLowerCase()\n : null)\n if (!name) continue\n\n const schema = def.inputSchema\n ? z.toJSONSchema(z.object(def.inputSchema)) as Record<string, unknown>\n : { type: 'object' as const, properties: {} }\n\n aiTools[name] = {\n description: def.description || '',\n inputSchema: jsonSchema(schema),\n execute: async (args: any) => {\n try {\n return await def.handler(args, {})\n } catch (error: any) {\n return { error: error.statusCode ? `[${error.statusCode}] ${error.message}` : error.message || String(error) }\n }\n }\n }\n }\n\n return aiTools\n}\n\n// NOTE: in-memory rate limit — not shared across multiple instances\nconst rateLimitMap = new Map<string, { count: number, resetAt: number }>()\nconst RATE_LIMIT_WINDOW_MS = 60_000\nconst RATE_LIMIT_MAX = 20\nconst MAX_MESSAGE_CONTENT_LENGTH = 32_000\n\n// Periodically purge expired entries to prevent unbounded memory growth\nsetInterval(() => {\n const now = Date.now()\n for (const [ip, entry] of rateLimitMap) {\n if (now > entry.resetAt) rateLimitMap.delete(ip)\n }\n}, RATE_LIMIT_WINDOW_MS)\n\nfunction checkRateLimit(ip: string) {\n const now = Date.now()\n const entry = rateLimitMap.get(ip)\n if (!entry || now > entry.resetAt) {\n rateLimitMap.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS })\n return\n }\n if (entry.count >= RATE_LIMIT_MAX) {\n const retryAfter = Math.ceil((entry.resetAt - now) / 1000)\n throw createError({ status: 429, message: 'Too many requests. Please try again later.', data: { retryAfter } })\n }\n entry.count++\n}\n\nexport default defineEventHandler(async (event) => {\n // Use socket remoteAddress as primary; x-forwarded-for only if behind trusted proxy\n const ip = event.node.req.socket?.remoteAddress\n ?? getHeader(event, 'x-forwarded-for')?.split(',')[0]?.trim()\n ?? 'unknown'\n checkRateLimit(ip)\n\n const { messages, restApiVersion, currentPage } = (await readBody<{ messages?: any, restApiVersion?: string, currentPage?: string }>(event)) || {}\n\n if (!messages || !Array.isArray(messages)) {\n throw createError({ status: 400, message: 'Invalid or missing messages array.' })\n }\n\n if (messages.length > 30) {\n throw createError({ status: 400, message: 'Too many messages in conversation.' })\n }\n\n for (const msg of messages) {\n const content = typeof msg?.content === 'string' ? msg.content : JSON.stringify(msg?.content ?? '')\n if (content.length > MAX_MESSAGE_CONTENT_LENGTH) {\n throw createError({ status: 400, message: 'Message content exceeds maximum allowed length.' })\n }\n }\n\n const safeRestApiVersion = restApiVersion === 'vue' ? 'vue' : 'nuxt'\n\n const safePage = currentPage\n ? currentPage.replace(/[^\\w\\-/. ]/gi, '').slice(0, 300)\n : undefined\n\n const mcpTools = mcpToolsToAiTools()\n\n const deepseek = createDeepSeek({\n apiKey: process.env.DEEPSEEK_API_KEY ?? ''\n })\n\n const abortController = new AbortController()\n event.node.req.on('close', () => abortController.abort())\n\n const system = `You are a helpful assistant for Bitrix24 JS SDK, a UI library for Nuxt and Vue. Use your knowledge base tools to search for relevant information before answering questions.\n\nThe user is using **${safeRestApiVersion === 'vue' ? 'Vue' : 'Nuxt'}**. Tailor your answers accordingly — ${safeRestApiVersion === 'vue' ? 'use the Vite plugin setup, Vue Router, and vite.config.ts instead of Nuxt-specific features like modules or app.config.ts. IMPORTANT: The Vite plugin auto-imports components and Bitrix24 JS SDK composables, but Vue core APIs and VueUse must be explicitly imported — always include these in code examples (e.g. `import { ref, computed } from \\'vue\\'`).' : 'use Nuxt modules, auto-imports, app.config.ts, and other Nuxt-specific features. Nuxt auto-imports Vue APIs (ref, computed, etc.), composables, and components — do not include these imports in code examples.'}\n${safePage ? `\\nThe user is currently viewing the documentation page at \\`${safePage}\\`. Use this context to provide more relevant answers (e.g. read that page first if the question seems related), but don't limit yourself to that page if the question is broader or unrelated.\\n` : ''}\nGuidelines:\n- For documentation questions, ALWAYS use tools to search for information. Never rely on pre-trained knowledge for Bitrix24 JS SDK APIs, props, or usage.\n— When users ask you to apply a theme change in real time (e.g., \"make it blue,\" \"create a Sakura theme,\" \"change the font\"), tell them that the library style matches the Bitrix24 style and suggest using standard properties (color, variant).\n- If a question is unrelated to Bitrix24 JS SDK (e.g. general coding, off-topic), briefly answer if you can, but don't waste tool calls searching docs for it.\n- If no relevant information is found after searching, respond with \"Sorry, I couldn't find information about that in the documentation.\"\n- Be concise and direct in your responses.\n\nGeneral Code Design Guidelines:\n- Try to use TypeScript wherever appropriate and practical.\n- When writing code, use SOLID approaches where practical and appropriate, but strive for simplicity. Try to avoid creating classes.\n\nCode Style Guidelines for Vue and Nuxt:\n- Use the B24 prefix (e.g. <B24Table> and so on) for Bitrix24 user interface components.\n- When constructing a list of items, you need to:\n - use Skeleton components to display the loading process\n - use the Empty component to display an empty list of items\n - use the Table component (framed by a Card component) to display the list of items\n\n**FORMATTING RULES (CRITICAL):**\n- ABSOLUTELY NO MARKDOWN HEADINGS: Never use #, ##, ###, ####, #####, or ######\n- NO underline-style headings with === or ---\n- Use **bold text** for emphasis and section labels instead\n- Examples:\n * Instead of \"## Usage\", write \"**Usage:**\" or just \"Here's how to use it:\"\n * Instead of \"# Complete Guide\", write \"**Complete Guide**\" or start directly with content\n- Start all responses with content, never with a heading\n\n- Reference specific component names, props, or APIs when applicable.\n- If a question is ambiguous, ask for clarification rather than guessing.\n- When multiple relevant items are found, list them clearly using bullet points.\n- You have up to ${maxStepCount} tool calls to find the answer, so be strategic: start broad, then get specific if needed.\n- Format responses in a conversational way, not as documentation sections.\n `\n\n return streamText({\n // @todo fix me deepseek-reasoner | deepseek-chat\n model: deepseek('deepseek-reasoner'),\n maxOutputTokens: 8000, // 10000 | 8100\n abortSignal: abortController.signal,\n providerOptions: {},\n system,\n messages: await convertToModelMessages(messages),\n experimental_transform: smoothStream(),\n stopWhen: stepCountIs(maxStepCount),\n tools: {\n ...mcpTools\n },\n onError: (error) => {\n console.error('streamText error:', error)\n }\n }).toUIMessageStreamResponse()\n})\n"}, "4fe82085a4a585c467cc83acde35f871f5317f24": {"t": 1785754321.146241, "n": 8508, "h": "62cdc6d2cc732c74", "c": "// @memo we not use jsonSchema\nimport { streamText, convertToModelMessages, smoothStream, stepCountIs, jsonSchema } from 'ai'\nimport { getHeader } from 'h3'\nimport { createDeepSeek } from '@ai-sdk/deepseek'\nimport { z } from 'zod'\nimport { tools as mcpToolDefinitions } from '#nuxt-mcp-toolkit/tools.mjs'\n\n/**\n * @docs https://ai-sdk.dev/providers/ai-sdk-providers/deepseek\n * @memo sync with: docs/content/docs/2.components/chat.md:297\n */\n\nconst maxStepCount = 5\n\n// async function initMcpToolsForUI() {\n// const httpTransport = new StreamableHTTPClientTransport(\n// new URL(`https://b24ui.bx-shef.by/b24ui/mcp/`)\n// )\n// const httpClient = await experimental_createMCPClient({\n// transport: httpTransport\n// })\n// return await httpClient.tools()\n// }\n//\n// async function initMcpToolsForB24RestApi() {\n// const httpTransport = new StreamableHTTPClientTransport(\n// new URL(`https://mcp-dev.bitrix24.tech/mcp`)\n// )\n// const httpClient = await experimental_createMCPClient({\n// transport: httpTransport\n// })\n// return await httpClient.tools()\n// }\n\nfunction mcpToolsToAiTools() {\n const aiTools: Record<string, { description: string, inputSchema: ReturnType<typeof jsonSchema>, execute: (args: any) => Promise<any> }> = {}\n\n for (const def of mcpToolDefinitions as any[]) {\n const filename = def._meta?.filename as string | undefined\n const name = def.name || (filename\n ? filename.replace(/\\.(ts|js|mts|mjs)$/, '').replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/[_\\s]+/g, '-').toLowerCase()\n : null)\n if (!name) continue\n\n const schema = def.inputSchema\n ? z.toJSONSchema(z.object(def.inputSchema)) as Record<string, unknown>\n : { type: 'object' as const, properties: {} }\n\n aiTools[name] = {\n description: def.description || '',\n inputSchema: jsonSchema(schema),\n execute: async (args: any) => {\n try {\n return await def.handler(args, {})\n } catch (error: any) {\n return { error: error.statusCode ? `[${error.statusCode}] ${error.message}` : error.message || String(error) }\n }\n }\n }\n }\n\n return aiTools\n}\n\n// NOTE: in-memory rate limit — not shared across multiple instances\nconst rateLimitMap = new Map<string, { count: number, resetAt: number }>()\nconst RATE_LIMIT_WINDOW_MS = 60_000\nconst RATE_LIMIT_MAX = 20\nconst MAX_MESSAGE_CONTENT_LENGTH = 32_000\n\n// Periodically purge expired entries to prevent unbounded memory growth\nsetInterval(() => {\n const now = Date.now()\n for (const [ip, entry] of rateLimitMap) {\n if (now > entry.resetAt) rateLimitMap.delete(ip)\n }\n}, RATE_LIMIT_WINDOW_MS)\n\nfunction checkRateLimit(ip: string) {\n const now = Date.now()\n const entry = rateLimitMap.get(ip)\n if (!entry || now > entry.resetAt) {\n rateLimitMap.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS })\n return\n }\n if (entry.count >= RATE_LIMIT_MAX) {\n const retryAfter = Math.ceil((entry.resetAt - now) / 1000)\n throw createError({ status: 429, message: 'Too many requests. Please try again later.', data: { retryAfter } })\n }\n entry.count++\n}\n\nexport default defineEventHandler(async (event) => {\n // Use socket remoteAddress as primary; x-forwarded-for only if behind trusted proxy\n const ip = event.node.req.socket?.remoteAddress\n ?? getHeader(event, 'x-forwarded-for')?.split(',')[0]?.trim()\n ?? 'unknown'\n checkRateLimit(ip)\n\n const { messages, restApiVersion, currentPage } = (await readBody<{ messages?: any, restApiVersion?: string, currentPage?: string }>(event)) || {}\n\n if (!messages || !Array.isArray(messages)) {\n throw createError({ status: 400, message: 'Invalid or missing messages array.' })\n }\n\n if (messages.length > 30) {\n throw createError({ status: 400, message: 'Too many messages in conversation.' })\n }\n\n for (const msg of messages) {\n const content = typeof msg?.content === 'string' ? msg.content : JSON.stringify(msg?.content ?? '')\n if (content.length > MAX_MESSAGE_CONTENT_LENGTH) {\n throw createError({ status: 400, message: 'Message content exceeds maximum allowed length.' })\n }\n }\n\n const safeRestApiVersion = restApiVersion === 'vue' ? 'vue' : 'nuxt'\n\n const safePage = currentPage\n ? currentPage.replace(/[^\\w\\-/. ]/gi, '').slice(0, 300)\n : undefined\n\n const mcpTools = mcpToolsToAiTools()\n\n const deepseek = createDeepSeek({\n apiKey: process.env.DEEPSEEK_API_KEY ?? ''\n })\n\n const abortController = new AbortController()\n event.node.req.on('close', () => abortController.abort())\n\n const system = `You are a helpful assistant for Bitrix24 JS SDK, a UI library for Nuxt and Vue. Use your knowledge base tools to search for relevant information before answering questions.\n\nThe user is using **${safeRestApiVersion === 'vue' ? 'Vue' : 'Nuxt'}**. Tailor your answers accordingly — ${safeRestApiVersion === 'vue' ? 'use the Vite plugin setup, Vue Router, and vite.config.ts instead of Nuxt-specific features like modules or app.config.ts. IMPORTANT: The Vite plugin auto-imports components and Bitrix24 JS SDK composables, but Vue core APIs and VueUse must be explicitly imported — always include these in code examples (e.g. `import { ref, computed } from \\'vue\\'`).' : 'use Nuxt modules, auto-imports, app.config.ts, and other Nuxt-specific features. Nuxt auto-imports Vue APIs (ref, computed, etc.), composables, and components — do not include these imports in code examples.'}\n${safePage ? `\\nThe user is currently viewing the documentation page at \\`${safePage}\\`. Use this context to provide more relevant answers (e.g. read that page first if the question seems related), but don't limit yourself to that page if the question is broader or unrelated.\\n` : ''}\nGuidelines:\n- For documentation questions, ALWAYS use tools to search for information. Never rely on pre-trained knowledge for Bitrix24 JS SDK APIs, props, or usage.\n— When users ask you to apply a theme change in real time (e.g., \"make it blue,\" \"create a Sakura theme,\" \"change the font\"), tell them that the library style matches the Bitrix24 style and suggest using standard properties (color, variant).\n- If a question is unrelated to Bitrix24 JS SDK (e.g. general coding, off-topic), briefly answer if you can, but don't waste tool calls searching docs for it.\n- If no relevant information is found after searching, respond with \"Sorry, I couldn't find information about that in the documentation.\"\n- Be concise and direct in your responses.\n\nGeneral Code Design Guidelines:\n- Try to use TypeScript wherever appropriate and practical.\n- When writing code, use SOLID approaches where practical and appropriate, but strive for simplicity. Try to avoid creating classes.\n\nCode Style Guidelines for Vue and Nuxt:\n- Use the B24 prefix (e.g. <B24Table> and so on) for Bitrix24 user interface components.\n- When constructing a list of items, you need to:\n - use Skeleton components to display the loading process\n - use the Empty component to display an empty list of items\n - use the Table component (framed by a Card component) to display the list of items\n\n**FORMATTING RULES (CRITICAL):**\n- ABSOLUTELY NO MARKDOWN HEADINGS: Never use #, ##, ###, ####, #####, or ######\n- NO underline-style headings with === or ---\n- Use **bold text** for emphasis and section labels instead\n- Examples:\n * Instead of \"## Usage\", write \"**Usage:**\" or just \"Here's how to use it:\"\n * Instead of \"# Complete Guide\", write \"**Complete Guide**\" or start directly with content\n- Start all responses with content, never with a heading\n\n- Reference specific component names, props, or APIs when applicable.\n- If a question is ambiguous, ask for clarification rather than guessing.\n- When multiple relevant items are found, list them clearly using bullet points.\n- You have up to ${maxStepCount} tool calls to find the answer, so be strategic: start broad, then get specific if needed.\n- Format responses in a conversational way, not as documentation sections.\n `\n\n return streamText({\n // @todo fix me deepseek-reasoner | deepseek-chat\n model: deepseek('deepseek-reasoner'),\n maxOutputTokens: 8000, // 10000 | 8100\n abortSignal: abortController.signal,\n providerOptions: {},\n system,\n messages: await convertToModelMessages(messages),\n experimental_transform: smoothStream(),\n stopWhen: stepCountIs(maxStepCount),\n tools: {\n ...mcpTools\n },\n onError: (error) => {\n console.error('streamText error:', error)\n }\n }).toUIMessageStreamResponse()\n})\n"}, "7d6da186ee51b3ec21b694c096a7410281472f0b": {"t": 1785754322.7907298, "n": 6664, "h": "3619d66c0b08d994", "c": "// @memo we not use jsonSchema\nimport { streamText, convertToModelMessages, smoothStream, stepCountIs, jsonSchema } from 'ai'\nimport { createDeepSeek } from '@ai-sdk/deepseek'\nimport { z } from 'zod'\nimport { tools as mcpToolDefinitions } from '#nuxt-mcp-toolkit/tools.mjs'\n\n/**\n * @docs https://ai-sdk.dev/providers/ai-sdk-providers/deepseek\n * @memo sync with: docs/content/docs/2.components/chat.md:297\n */\n\nconst maxStepCount = 5\n\n// async function initMcpToolsForUI() {\n// const httpTransport = new StreamableHTTPClientTransport(\n// new URL(`https://b24ui.bx-shef.by/b24ui/mcp/`)\n// )\n// const httpClient = await experimental_createMCPClient({\n// transport: httpTransport\n// })\n// return await httpClient.tools()\n// }\n//\n// async function initMcpToolsForB24RestApi() {\n// const httpTransport = new StreamableHTTPClientTransport(\n// new URL(`https://mcp-dev.bitrix24.tech/mcp`)\n// )\n// const httpClient = await experimental_createMCPClient({\n// transport: httpTransport\n// })\n// return await httpClient.tools()\n// }\n\nfunction mcpToolsToAiTools() {\n const aiTools: Record<string, { description: string, inputSchema: ReturnType<typeof jsonSchema>, execute: (args: any) => Promise<any> }> = {}\n\n for (const def of mcpToolDefinitions as any[]) {\n const filename = def._meta?.filename as string | undefined\n const name = def.name || (filename\n ? filename.replace(/\\.(ts|js|mts|mjs)$/, '').replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/[_\\s]+/g, '-').toLowerCase()\n : null)\n if (!name) continue\n\n const schema = def.inputSchema\n ? z.toJSONSchema(z.object(def.inputSchema)) as Record<string, unknown>\n : { type: 'object' as const, properties: {} }\n\n aiTools[name] = {\n description: def.description || '',\n inputSchema: jsonSchema(schema),\n execute: async (args: any) => {\n try {\n return await def.handler(args, {})\n } catch (error: any) {\n return { error: error.statusCode ? `[${error.statusCode}] ${error.message}` : error.message || String(error) }\n }\n }\n }\n }\n\n return aiTools\n}\n\nexport default defineEventHandler(async (event) => {\n const { messages, restApiVersion, currentPage } = (await readBody<{ messages?: any, restApiVersion?: string, currentPage?: string }>(event)) || {}\n\n if (!messages || !Array.isArray(messages)) {\n throw createError({ status: 400, message: 'Invalid or missing messages array.' })\n }\n\n const mcpTools = mcpToolsToAiTools()\n\n const deepseek = createDeepSeek({\n apiKey: process.env.DEEPSEEK_API_KEY ?? ''\n })\n\n const abortController = new AbortController()\n event.node.req.on('close', () => abortController.abort())\n\n const system = `You are a helpful assistant for Bitrix24 JS SDK, a UI library for Nuxt and Vue. Use your knowledge base tools to search for relevant information before answering questions.\n\nThe user is using **${restApiVersion === 'vue' ? 'Vue' : 'Nuxt'}**. Tailor your answers accordingly — ${restApiVersion === 'vue' ? 'use the Vite plugin setup, Vue Router, and vite.config.ts instead of Nuxt-specific features like modules or app.config.ts. IMPORTANT: The Vite plugin auto-imports components and Bitrix24 JS SDK composables, but Vue core APIs and VueUse must be explicitly imported — always include these in code examples (e.g. `import { ref, computed } from \\'vue\\'`).' : 'use Nuxt modules, auto-imports, app.config.ts, and other Nuxt-specific features. Nuxt auto-imports Vue APIs (ref, computed, etc.), composables, and components — do not include these imports in code examples.'}\n${currentPage ? `\\nThe user is currently viewing the documentation page at \\`${currentPage}\\`. Use this context to provide more relevant answers (e.g. read that page first if the question seems related), but don't limit yourself to that page if the question is broader or unrelated.\\n` : ''}\nGuidelines:\n- For documentation questions, ALWAYS use tools to search for information. Never rely on pre-trained knowledge for Bitrix24 JS SDK APIs, props, or usage.\n— When users ask you to apply a theme change in real time (e.g., \"make it blue,\" \"create a Sakura theme,\" \"change the font\"), tell them that the library style matches the Bitrix24 style and suggest using standard properties (color, variant).\n- If a question is unrelated to Bitrix24 JS SDK (e.g. general coding, off-topic), briefly answer if you can, but don't waste tool calls searching docs for it.\n- If no relevant information is found after searching, respond with \"Sorry, I couldn't find information about that in the documentation.\"\n- Be concise and direct in your responses.\n\nGeneral Code Design Guidelines:\n- Try to use TypeScript wherever appropriate and practical.\n- When writing code, use SOLID approaches where practical and appropriate, but strive for simplicity. Try to avoid creating classes.\n\nCode Style Guidelines for Vue and Nuxt:\n- Use the B24 prefix (e.g. <B24Table> and so on) for Bitrix24 user interface components.\n- When constructing a list of items, you need to:\n - use Skeleton components to display the loading process\n - use the Empty component to display an empty list of items\n - use the Table component (framed by a Card component) to display the list of items\n\n**FORMATTING RULES (CRITICAL):**\n- ABSOLUTELY NO MARKDOWN HEADINGS: Never use #, ##, ###, ####, #####, or ######\n- NO underline-style headings with === or ---\n- Use **bold text** for emphasis and section labels instead\n- Examples:\n * Instead of \"## Usage\", write \"**Usage:**\" or just \"Here's how to use it:\"\n * Instead of \"# Complete Guide\", write \"**Complete Guide**\" or start directly with content\n- Start all responses with content, never with a heading\n\n- Reference specific component names, props, or APIs when applicable.\n- If a question is ambiguous, ask for clarification rather than guessing.\n- When multiple relevant items are found, list them clearly using bullet points.\n- You have up to ${maxStepCount} tool calls to find the answer, so be strategic: start broad, then get specific if needed.\n- Format responses in a conversational way, not as documentation sections.\n `\n\n return streamText({\n // @todo fix me deepseek-reasoner | deepseek-chat\n model: deepseek('deepseek-reasoner'),\n maxOutputTokens: 8000, // 10000 | 8100\n abortSignal: abortController.signal,\n providerOptions: {},\n system,\n messages: await convertToModelMessages(messages),\n experimental_transform: smoothStream(),\n stopWhen: stepCountIs(maxStepCount),\n tools: {\n ...mcpTools\n },\n onError: (error) => {\n console.error('streamText error:', error)\n }\n }).toUIMessageStreamResponse()\n})\n"}}}}