UNPKG

solver-sdk

Version:

SDK для интеграции с Code Solver Backend API (совместимо с браузером и Node.js), с поддержкой функциональности мышления (Thinking Mode)

511 lines 28.5 kB
/** * API для работы с чатом */ export class ChatApi { /** * Создает новый экземпляр API для работы с чатом * @param {HttpClient} httpClient HTTP клиент */ constructor(httpClient) { this.httpClient = httpClient; } /** * Отправляет сообщение в чат и получает ответ от модели * @param {ChatMessage[]} messages Массив сообщений для отправки * @param {ChatOptions} [options] Дополнительные параметры * @returns {Promise<ChatResponse>} Ответ модели */ async chat(messages, options) { if (!messages || messages.length === 0) { throw new Error('Необходимо предоставить хотя бы одно сообщение'); } // Проверяем наличие хотя бы одного сообщения от пользователя const hasUserMessage = messages.some(msg => msg.role === 'user'); if (!hasUserMessage) { throw new Error('В сообщениях должно быть хотя бы одно сообщение с ролью "user"'); } // Отправляем запрос к API чата return this.httpClient.post('/api/v1/chat', { model: options?.model || 'Claude', messages, temperature: options?.temperature, maxTokens: options?.maxTokens, stopSequences: options?.stopSequences, functions: options?.functions, functionCall: options?.functionCall, thinking: options?.thinking, region: options?.region }); } /** * Алиас для метода chat для совместимости с другими SDK * @param {ChatMessage[]} messages Массив сообщений для отправки * @param {ChatOptions} [options] Дополнительные параметры * @returns {Promise<ChatResponse>} Ответ модели */ async chatCompletion(messages, options) { return this.chat(messages, options); } /** * Проверяет доступность API чата * @returns {Promise<boolean>} Результат проверки */ async checkAvailability() { try { // Используем HEAD запрос вместо GET, так как эндпоинт поддерживает только HEAD/POST await this.httpClient.request({ method: 'HEAD', url: '/api/v1/chat' }); return true; } catch (error) { return false; } } /** * Отправляет сообщение в чат и получает ответ от модели * с автоматическим переключением между регионами при ошибках перегрузки * @param {ChatMessage[]} messages Массив сообщений для отправки * @param {ChatOptions} [options] Дополнительные параметры * @returns {Promise<ChatResponse>} Ответ модели */ async chatWithRegionFailover(messages, options) { // Список всех доступных регионов const allRegions = ['us-east-1', 'eu-west-1', 'ap-southeast-2']; // Начинаем с региона из параметров, или первого в списке let startRegionIndex = 0; if (options?.region) { const regionIndex = allRegions.indexOf(options.region); if (regionIndex !== -1) { startRegionIndex = regionIndex; } } // Реорганизуем массив, чтобы начать с указанного региона const regions = [ ...allRegions.slice(startRegionIndex), ...allRegions.slice(0, startRegionIndex) ]; // Последняя ошибка, будет возвращена если все регионы недоступны let lastError = null; // Пробуем каждый регион по очереди for (let i = 0; i < regions.length; i++) { const region = regions[i]; try { console.log(`Попытка запроса к Anthropic API в регионе ${region}`); // Копируем опции и устанавливаем текущий регион const regionOptions = { ...options, region }; // Отправляем запрос с конкретным регионом return await this.chat(messages, regionOptions); } catch (error) { lastError = error; // Проверяем, является ли ошибка ошибкой перегрузки (код 529) const isOverloadError = error.status === 529 || error.code === 529 || (error.response?.status === 529) || (error.message && error.message.includes('overloaded')) || (error.error?.type === 'overloaded_error'); if (isOverloadError) { console.warn(`Регион ${region} перегружен, пробуем следующий регион`); // Продолжаем цикл и пробуем следующий регион continue; } else { // Если ошибка не связана с перегрузкой, прекращаем попытки console.error(`Ошибка в регионе ${region}, не связанная с перегрузкой:`, error); throw error; } } } // Если мы здесь, значит все регионы перегружены throw lastError || new Error('Все регионы Anthropic API перегружены, попробуйте позже'); } /** * Отправляет одиночный запрос к модели с автоматическим переключением регионов * @param {string} prompt Запрос к модели * @param {ChatOptions} [options] Дополнительные параметры * @returns {Promise<string>} Текстовый ответ модели */ async sendPromptWithRegionFailover(prompt, options) { const messages = [ { role: 'user', content: prompt } ]; const response = await this.chatWithRegionFailover(messages, options); if (response.choices && response.choices.length > 0) { return response.choices[0].message.content; } throw new Error('Модель не вернула ответ'); } /** * Отправляет сообщение в чат в потоковом режиме * @param {ChatMessage[]} messages Массив сообщений для отправки * @param {ChatStreamOptions} [options] Дополнительные параметры * @returns {AsyncGenerator<ChatStreamChunk>} Асинхронный генератор чанков ответа */ async *streamChat(messages, options) { if (!messages || messages.length === 0) { throw new Error('Необходимо предоставить хотя бы одно сообщение'); } // Проверяем наличие хотя бы одного сообщения от пользователя const hasUserMessage = messages.some(msg => msg.role === 'user'); if (!hasUserMessage) { throw new Error('В сообщениях должно быть хотя бы одно сообщение с ролью "user"'); } try { // Создаем параметры запроса const params = { model: options?.model || 'Claude', messages, temperature: options?.temperature, maxTokens: options?.maxTokens, stopSequences: options?.stopSequences, functions: options?.functions, functionCall: options?.functionCall, thinking: options?.thinking, stream: true }; // Получаем поток данных от API const response = await this.httpClient.request({ url: '/api/v1/chat/stream', method: 'POST', data: params, headers: { 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache' } }); if (!response || !response.body) { throw new Error('Сервер не вернул поток данных'); } // Обрабатываем поток событий const reader = response.body.getReader(); const decoder = new TextDecoder(); let isInThinkingBlock = false; let thinkingContent = ''; let textContent = ''; let thinkingSignature = ''; while (true) { const { done, value } = await reader.read(); if (done) { break; } const chunk = decoder.decode(value); const lines = chunk.split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (!line.startsWith('data:')) continue; const data = line.slice(5).trim(); if (data === '[DONE]') { // Поток завершен if (options?.onComplete) { options.onComplete(textContent); } yield { text: '', isComplete: true, thinkingContent: thinkingContent.length > 0 ? thinkingContent : undefined, thinkingSignature: thinkingSignature.length > 0 ? thinkingSignature : undefined }; return; } try { const parsedData = JSON.parse(data); // Обрабатываем различные типы событий if (parsedData.type === 'thinking_start' || parsedData.type === 'content_block_start' && parsedData.content_block?.type === 'thinking') { isInThinkingBlock = true; // Отправляем начало блока thinking if (options?.onToken) { options.onToken('[THINKING_START]'); } yield { text: '[THINKING_START]', isComplete: false, isThinking: true }; } else if (parsedData.type === 'thinking_end' || parsedData.type === 'content_block_stop' && isInThinkingBlock) { isInThinkingBlock = false; // Отправляем конец блока thinking if (options?.onToken) { options.onToken('[THINKING_END]'); } yield { text: '[THINKING_END]', isComplete: false, isThinking: true }; } else if (parsedData.type === 'content_block_delta' || parsedData.type === 'thinking_delta') { const text = parsedData.delta?.text || ''; if (isInThinkingBlock || parsedData.type === 'thinking_delta') { // Добавляем текст к блоку размышлений thinkingContent += text; if (options?.onToken) { options.onToken(text); } yield { text, isComplete: false, isThinking: true }; } else { // Добавляем текст к основному содержимому textContent += text; if (options?.onToken) { options.onToken(text); } yield { text, isComplete: false, isThinking: false }; } } } catch (e) { console.error('Ошибка при парсинге данных:', e); } } } // Финальный чанк, если поток завершился без [DONE] if (options?.onComplete) { options.onComplete(textContent); } yield { text: '', isComplete: true, thinkingContent: thinkingContent.length > 0 ? thinkingContent : undefined, thinkingSignature: thinkingSignature.length > 0 ? thinkingSignature : undefined }; } catch (error) { // Проверяем наличие ошибки географических ограничений const errorObj = error; const isGeoRestriction = (errorObj.status === 403 || errorObj.code === 403) && errorObj.message && (errorObj.message.includes('ограничен в вашем регионе') || errorObj.message.includes('Request not allowed') || errorObj.message.includes('forbidden')); if (isGeoRestriction) { console.error('\n=============================================='); console.error('⚠️ ОШИБКА ГЕОГРАФИЧЕСКОГО ОГРАНИЧЕНИЯ API ANTHROPIC'); console.error('⚠️ Для работы с API Anthropic требуется VPN или прокси'); console.error('⚠️ Anthropic API доступен только из определенных регионов'); console.error('=============================================='); // Обогащаем объект ошибки информацией о географических ограничениях errorObj.type = 'geo_restriction'; } if (options?.onError) { options.onError(errorObj); } throw errorObj; } } /** * Отправляет запрос к модели в потоковом режиме (упрощенный интерфейс) * @param {string} prompt Запрос к модели * @param {ChatStreamOptions} [options] Дополнительные параметры * @returns {AsyncGenerator<ChatStreamChunk>} Асинхронный генератор чанков ответа */ async *streamPrompt(prompt, options) { const messages = [ { role: 'user', content: prompt } ]; yield* this.streamChat(messages, options); } /** * Создает новое WebSocket соединение для потокового чата * @returns {Promise<WebSocketConnectResponse>} Информация о созданном соединении */ async connectWebSocket() { return this.httpClient.post('/api/v1/chat/connect'); } /** * Отправляет сообщение в чат в потоковом режиме с поддержкой thinking через WebSocket * @param {ChatMessage[]} messages Массив сообщений для отправки * @param {ChatStreamOptions} [options] Дополнительные параметры * @param {(eventType: string, data: any) => void} [onEvent] Обработчик событий WebSocket * @returns {Promise<ThinkingStreamResponse>} Информация о потоковом запросе */ async streamChatWithThinking(messages, options = {}, onEvent) { if (!messages || messages.length === 0) { throw new Error('Необходимо предоставить хотя бы одно сообщение'); } try { // 1. Создаем параметры для запроса const socketId = `socket_${Date.now()}_${this.generateId(10)}`; let socket = null; // 2. Подключаемся к WebSocket и ждем успешного подключения if (onEvent) { await new Promise((resolve, reject) => { try { const socketOptions = { transports: ['websocket'], reconnection: true, reconnectionAttempts: 3, reconnectionDelay: 1000, timeout: 10000, query: { socketId: socketId, token: options.authToken || 'test-token' } }; // Динамически импортируем socket.io-client import('socket.io-client').then((socketIoModule) => { const io = socketIoModule.default || socketIoModule.io; if (!io) { reject(new Error('Не удалось импортировать socket.io-client')); return; } // Создаем соединение с сервером WebSocket const serverUrl = this.httpClient.getBaseURL() || 'http://localhost:3000'; socket = io(`${serverUrl}/reasoning`, socketOptions); // Устанавливаем обработчики событий socket.on('connect', () => { console.log(`[SDK] WebSocket подключен: ${socket.id}, задано socketId: ${socketId}`); // При подключении отправляем событие аутентификации socket.emit('authenticate', { socketId: socketId, token: options.authToken || 'test-token' }); // Отправляем событие присоединения к комнате с ID socket.emit('join_room', { roomId: socketId }); // Вызываем обработчик события connect if (onEvent) { onEvent('connect', { socketId }); } // Разрешаем промис, продолжаем выполнение resolve(); }); // Обработчик сообщений от сервера socket.on('message', (data) => { if (onEvent && data && data.type) { onEvent(data.type, data.data || data); } }); // Обработчик ошибок socket.on('error', (error) => { console.error('[SDK] WebSocket error:', error); // Проверяем наличие ошибки географических ограничений const isGeoRestriction = (error.type === 'geo_restriction' || error.code === 403) && error.message && (error.message.includes('ограничен в вашем регионе') || error.message.includes('Request not allowed')); if (isGeoRestriction) { console.error('\n=============================================='); console.error('⚠️ ОШИБКА ГЕОГРАФИЧЕСКОГО ОГРАНИЧЕНИЯ API ANTHROPIC'); console.error('⚠️ Для работы с API Anthropic требуется VPN или прокси'); console.error('⚠️ Anthropic API доступен только из определенных регионов'); console.error('=============================================='); } if (onEvent) { onEvent('error', { message: error.message || 'Unknown WebSocket error', code: error.code, type: isGeoRestriction ? 'geo_restriction' : (error.type || 'websocket_error') }); } }); // Обработчик отключения socket.on('disconnect', (reason) => { console.log(`[SDK] WebSocket отключен: ${reason}`); if (onEvent) { onEvent('disconnect', { reason }); } }); // Обработчики для событий аутентификации socket.on('authenticated', (data) => { console.log('[SDK] Аутентификация успешна', data); if (onEvent) { onEvent('authenticated', data); } }); socket.on('authentication_error', (data) => { console.error('[SDK] Ошибка аутентификации:', data); if (onEvent) { onEvent('error', { message: 'Ошибка аутентификации: ' + (data.message || 'Unknown error'), code: 'AUTH_ERROR' }); } reject(new Error('Ошибка аутентификации: ' + (data.message || 'Unknown error'))); }); // Регистрируем обработчики для всех событий потока мышления ['message_start', 'content_block_start', 'thinking_start', 'thinking_delta', 'text_delta', 'content_block_stop', 'message_stop', 'done'].forEach(eventType => { socket.on(eventType, (data) => { if (onEvent) { onEvent(eventType, data); } }); }); // Обработчик таймаута соединения setTimeout(() => { if (socket && !socket.connected) { reject(new Error('WebSocket connection timeout')); } }, 10000); }).catch(error => { const errorMessage = error instanceof Error ? error.message : String(error); reject(new Error(`Ошибка импорта socket.io-client: ${errorMessage}`)); }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); reject(new Error(`Ошибка WebSocket: ${errorMessage}`)); } }); } // 3. Отправляем HTTP запрос для инициации потока return this.httpClient.post('/api/v1/chat/stream-chat', { model: options.model || 'claude-3-7-sonnet-20240229', messages, temperature: options.temperature, maxTokens: options.maxTokens, socketId: socketId, thinking: true, region: options.region }, { 'X-Socket-ID': socketId }); } catch (error) { // Проверяем наличие ошибки географических ограничений const errorObj = error; const isGeoRestriction = (errorObj.status === 403 || errorObj.code === 403) && errorObj.message && (errorObj.message.includes('ограничен в вашем регионе') || errorObj.message.includes('Request not allowed') || errorObj.message.includes('forbidden')); if (isGeoRestriction) { console.error('\n=============================================='); console.error('⚠️ ОШИБКА ГЕОГРАФИЧЕСКОГО ОГРАНИЧЕНИЯ API ANTHROPIC'); console.error('⚠️ Для работы с API Anthropic требуется VPN или прокси'); console.error('⚠️ Anthropic API доступен только из определенных регионов'); console.error('=============================================='); // Обогащаем объект ошибки информацией о географических ограничениях errorObj.type = 'geo_restriction'; } if (options.onError) { options.onError(errorObj); } throw errorObj; } } /** * Генерирует случайный ID указанной длины * @param {number} length Длина ID * @returns {string} Сгенерированный ID */ generateId(length) { const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } } //# sourceMappingURL=chat-api.js.map