solver-sdk
Version:
SDK для интеграции с Code Solver Backend API (совместимо с браузером и Node.js), с поддержкой функциональности мышления (Thinking Mode)
511 lines • 28.5 kB
JavaScript
/**
* 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