solver-sdk
Version:
SDK для интеграции с Code Solver Backend API
348 lines • 17.8 kB
JavaScript
import { generateId, setupSocketEventHandlers } from './websocket-helpers';
import { handleStreamError, processStreamChunk } from './stream-utils';
// Экспортируем все типы и интерфейсы для внешнего использования
export * from './models';
export * from './interfaces';
/**
* API для работы с чатом
*/
export class ChatApi {
/**
* Создает новый экземпляр API для работы с чатом
* @param {IHttpClient} 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 result = processStreamChunk(line, isInThinkingBlock, thinkingContent, textContent, thinkingSignature, options?.onToken);
isInThinkingBlock = result.isInThinkingBlock;
thinkingContent = result.thinkingContent;
textContent = result.textContent;
thinkingSignature = result.thinkingSignature;
if (result.chunk) {
yield result.chunk;
}
if (result.isDone) {
// Поток завершен
if (options?.onComplete) {
options.onComplete(textContent);
}
return;
}
}
}
// Финальный чанк, если поток завершился без [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 = handleStreamError(error);
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 {EventHandler} [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()}_${generateId(10)}`;
// Создаем sessionId для отслеживания контекста между запросами
const sessionId = options.sessionId || `thinking-${Date.now()}`;
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);
// Устанавливаем обработчики событий
setupSocketEventHandlers(socket, socketId, sessionId, onEvent);
// Разрешаем промис, продолжаем выполнение
resolve();
// Обработчик таймаута соединения
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,
sessionId: sessionId, // Добавляем sessionId для кэширования блоков мышления
thinking: true,
region: options.region
}, {
'X-Socket-ID': socketId,
'X-Session-ID': sessionId // Добавляем ID сессии в заголовки
});
}
catch (error) {
const errorObj = handleStreamError(error);
if (options.onError) {
options.onError(errorObj);
}
throw errorObj;
}
}
}
//# sourceMappingURL=index.js.map