mnemos-coder
Version:
CLI-based coding agent with graph-based execution loop and terminal UI
217 lines • 9.78 kB
JavaScript
/**
* LLM Client for communicating with Qwen models
* Based on seahorse llm_client.py
*/
import fetch from 'node-fetch';
import { getLogger } from './utils/Logger.js';
export class LLMClient {
config;
logger;
constructor(config) {
this.config = config;
this.logger = getLogger('LLMClient');
}
async chat(messages, options = {}) {
try {
// Log the full request being sent to LLM
const requestBody = {
model: this.config.model,
messages,
temperature: options.temperature ?? this.config.temperature,
max_tokens: options.max_tokens ?? this.config.max_tokens,
stream: false
};
this.logger.debug('Sending request to LLM', {
url: `${this.config.base_url}/chat/completions`,
model: requestBody.model,
temperature: requestBody.temperature,
max_tokens: requestBody.max_tokens,
messageCount: messages.length
});
// Log full messages array with system prompts
this.logger.trace('Full LLM request messages:', {
messages: messages.map(m => ({
role: m.role,
contentLength: m.content.length,
contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? '...' : '')
}))
});
// Log complete system prompt if present
const systemMessage = messages.find(m => m.role === 'system');
if (systemMessage) {
this.logger.trace('System prompt sent to LLM:', {
systemPrompt: systemMessage.content
});
}
const response = await fetch(`${this.config.base_url}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.config.api_key}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
this.logger.error('LLM request failed', {
status: response.status,
statusText: response.statusText,
errorBody: errorText
});
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
}
const data = await response.json();
const responseContent = data.choices?.[0]?.message?.content || '';
// Log the full response from LLM
this.logger.debug('Received response from LLM', {
responseLength: responseContent.length,
hasContent: !!responseContent,
usage: data.usage
});
// Log response content safely (it could be very large)
if (responseContent.length > 5000) {
this.logger.trace('Full LLM response content (truncated):', {
contentLength: responseContent.length,
contentPreview: responseContent.substring(0, 2000) + '...[truncated]'
});
}
else {
this.logger.trace('Full LLM response content:', {
content: responseContent
});
}
// Check if response contains tool calls
const hasToolCalls = responseContent.includes('<tool_call>');
this.logger.debug('Response analysis', {
hasToolCalls,
toolCallCount: hasToolCalls ? (responseContent.match(/<tool_call>/g) || []).length : 0
});
return responseContent;
}
catch (error) {
this.logger.error('LLM request failed with exception', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
});
console.error('LLM request failed:', error);
throw error;
}
}
async *streamChat(messages, options = {}) {
try {
// Log the full request being sent to LLM
const requestBody = {
model: this.config.model,
messages,
temperature: options.temperature ?? this.config.temperature,
max_tokens: options.max_tokens ?? this.config.max_tokens,
stream: true
};
this.logger.debug('Sending streaming request to LLM', {
url: `${this.config.base_url}/chat/completions`,
model: requestBody.model,
temperature: requestBody.temperature,
max_tokens: requestBody.max_tokens,
messageCount: messages.length
});
// Log full messages array with system prompts
this.logger.trace('Full LLM streaming request messages:', {
messages: messages.map(m => ({
role: m.role,
contentLength: m.content.length,
contentPreview: m.content.substring(0, 500) + (m.content.length > 500 ? '...' : '')
}))
});
// Log complete system prompt if present
const systemMessage = messages.find(m => m.role === 'system');
if (systemMessage) {
this.logger.debug('System prompt sent to LLM (streaming):', {
systemPrompt: systemMessage.content
});
}
const response = await fetch(`${this.config.base_url}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.config.api_key}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
this.logger.error('LLM streaming request failed', {
status: response.status,
statusText: response.statusText,
errorBody: errorText
});
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
}
if (!response.body) {
throw new Error('No response body');
}
// For Node.js environments, response.body is a Readable stream
let buffer = '';
let fullResponse = '';
for await (const chunk of response.body) {
buffer += chunk.toString();
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '')
continue;
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
// Log the complete streamed response
this.logger.info('Streaming response completed', {
totalLength: fullResponse.length,
hasToolCalls: fullResponse.includes('<tool_call>'),
toolCallCount: (fullResponse.match(/<tool_call>/g) || []).length
});
// Log response content safely (it could be very large)
if (fullResponse.length > 5000) {
this.logger.debug('Full streamed response (truncated):', {
contentLength: fullResponse.length,
contentPreview: fullResponse.substring(0, 2000) + '...[truncated]'
});
}
else {
this.logger.debug('Full streamed response:', {
content: fullResponse
});
}
continue;
}
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content;
if (content) {
fullResponse += content;
yield {
type: 'content',
content
};
}
}
catch (parseError) {
this.logger.warn('Failed to parse SSE data', {
error: parseError instanceof Error ? parseError.message : String(parseError),
data: data
});
console.warn('Failed to parse SSE data:', parseError);
}
}
}
}
}
catch (error) {
this.logger.error('Stream chat failed with exception', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
});
console.error('Stream chat failed:', error);
throw error;
}
}
}
//# sourceMappingURL=llm-client.js.map