UNPKG

@nomyx/assistant

Version:

A powerful assistant library and cli for your AI projects. works with Vertex AI (Claude and Gemini)

238 lines (234 loc) 9.07 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.streamChat = streamChat; const axios_1 = __importDefault(require("axios")); const tools_1 = require("./tools"); const modelConfig_1 = require("./modelConfig"); function convertContentToOpenRouterFormat(content) { return content; } async function* streamChat(config, messages, options, tools) { const url = config.getApiUrl('chat/completions'); const model = config.model; const logger = config.getLogger(); const requestBody = { messages: (0, tools_1.convertMessages)(messages).map(msg => ({ ...msg, content: convertContentToOpenRouterFormat(Array.isArray(msg.content) ? msg.content.map((part) => part.text).join(' ') : msg.content) })), model: model, max_tokens: options.maxTokens || (0, modelConfig_1.getMaxTokens)(model), temperature: options.temperature, stream: true }; const modelSupportsTools = (0, modelConfig_1.supportsTools)(model); if (tools && tools.length > 0) { if (modelSupportsTools) { requestBody.tools = tools.map(tool => (0, tools_1.convertToolSchema)(tool.getSchema())); requestBody.tool_choice = 'auto'; } else { // Append instructions for using tools const toolInstructions = ` You have access to the following tools: ${tools.map(tool => `- ${tool.name}: ${tool.description}`).join('\n')} To use a tool, output a function call in a code block like this: \`\`\`function_call { "name": "tool_name", "arguments": { "arg1": "value1", "arg2": "value2" } } \`\`\` Replace "tool_name" with the actual tool name and provide the necessary arguments. For example, to use the "tool_name" tool with arguments "arg1" and "arg2": \`\`\`function_call { "name": "tool_name", "arguments": { "arg1": "value1", "arg2": "value2" } } \`\`\` `; requestBody.messages[requestBody.messages.length - 1].content += '\n\n' + toolInstructions; } } const headers = { ...config.getHeaders(), 'HTTP-Referer': config.httpReferer, 'X-Title': config.xTitle }; logger.debug('OpenRouter API streaming request', { url, model, toolsCount: tools?.length }); try { const response = await axios_1.default.post(url, requestBody, { headers, responseType: 'stream', }); console.clear(); logger.debug('OpenRouter API streaming response received'); let buffer = ''; let accumulatedMessage = { role: 'assistant', content: '', tool_calls: [] }; for await (const chunk of response.data) { buffer += chunk.toString(); let newlineIndex; while ((newlineIndex = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, newlineIndex).trim(); buffer = buffer.slice(newlineIndex + 1); if (line.startsWith('data: ')) { const jsonData = line.slice(6); //logger.debug('Received JSON data', { jsonData }); if (jsonData === '[DONE]') { //logger.debug('End of stream detected'); return; } try { const data = JSON.parse(jsonData); yield* createStreamingProviderResponse(data, modelSupportsTools, logger, accumulatedMessage); } catch (error) { logger.error('Error parsing JSON', { line, error }); } } } } } catch (error) { if (axios_1.default.isAxiosError(error)) { logger.error('OpenRouter API streaming error', { status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data }); } else { logger.error('OpenRouter API streaming error', { error }); } throw error; } } function* createStreamingProviderResponse(data, modelSupportsTools, logger, accumulatedMessage) { const choice = data.choices[0]; if (!choice) { logger.error('Unexpected response format from OpenRouter API: No choices', { data }); throw new Error('Unexpected response format from OpenRouter API: No choices'); } if (choice.delta) { // Handle streaming format updateAccumulatedMessage(accumulatedMessage, choice.delta); } else if (choice.message) { // Handle non-streaming format accumulatedMessage = ensureValidOpenRouterMessage(choice.message); } else { logger.error('Unexpected response format from OpenRouter API: No delta or message in choice', { choice }); throw new Error('Unexpected response format from OpenRouter API: No delta or message in choice'); } let content = getContentAsString(accumulatedMessage.content); let toolCalls = modelSupportsTools ? (0, tools_1.extractToolCalls)(accumulatedMessage) : []; if (!modelSupportsTools) { const functionCallRegex = /```function_call\n([\s\S]*?)```/g; let match; while ((match = functionCallRegex.exec(content)) !== null) { try { const functionCall = JSON.parse(match[1]); toolCalls.push({ name: functionCall.name, arguments: functionCall.arguments }); } catch (error) { logger.error('Error parsing function call', { error, match: match[1] }); } } content = content.replace(functionCallRegex, ''); } yield { content: content.trim(), toolCalls: toolCalls, usage: { promptTokens: data.usage?.prompt_tokens || 0, completionTokens: data.usage?.completion_tokens || 0, totalTokens: data.usage?.total_tokens || 0 }, }; } function updateAccumulatedMessage(accumulatedMessage, delta) { if (delta.role && (delta.role === 'assistant' || delta.role === 'user' || delta.role === 'system' || delta.role === 'tool')) { accumulatedMessage.role = delta.role; } if (delta.content) { if (typeof accumulatedMessage.content === 'string') { accumulatedMessage.content += delta.content; process.stdout.write(delta.content); } else if (Array.isArray(accumulatedMessage.content)) { if (accumulatedMessage.content.length === 0 || typeof accumulatedMessage.content[accumulatedMessage.content.length - 1] !== 'string') { accumulatedMessage.content.push({ type: 'text', text: delta.content }); } else { const lastPart = accumulatedMessage.content[accumulatedMessage.content.length - 1]; if (lastPart.type === 'text') { lastPart.text += delta.content; } else { accumulatedMessage.content.push({ type: 'text', text: delta.content }); } } } } if (delta.tool_calls) { if (!accumulatedMessage.tool_calls) { accumulatedMessage.tool_calls = []; } delta.tool_calls.forEach((toolCall, index) => { if (!accumulatedMessage.tool_calls[index]) { accumulatedMessage.tool_calls[index] = { id: '', type: 'function', function: { name: '', arguments: '' } }; } if (toolCall.id) { accumulatedMessage.tool_calls[index].id = toolCall.id; } if (toolCall.function) { if (toolCall.function.name) { accumulatedMessage.tool_calls[index].function.name = toolCall.function.name; } if (toolCall.function.arguments) { accumulatedMessage.tool_calls[index].function.arguments += toolCall.function.arguments; } } }); } } function getContentAsString(content) { if (typeof content === 'string') { return content; } else if (Array.isArray(content)) { return content.map(part => { if (part.type === 'text') { return part.text; } else if (part.type === 'image_url') { return `[Image: ${part.image_url.url}]`; } return ''; }).join(' '); } return ''; } function ensureValidOpenRouterMessage(message) { const validRoles = ['assistant', 'user', 'system', 'tool']; const role = validRoles.includes(message.role) ? message.role : 'assistant'; return { role, content: message.content || '', tool_calls: message.tool_calls || [] }; }