import * as https from 'https';
import * as http from 'http';
import { ChatMessage, AIConfig, AIResponse, ToolDefinition, ToolCall } from '../common/types';
export class OpenAICompatClient {
async chat(
messages: ChatMessage[],
config: AIConfig,
tools?: ToolDefinition[],
onChunk?: (content: string) => void
): Promise<AIResponse> {
const url = new URL(`${config.baseUrl}/v1/chat/completions`);
const isHttps = url.protocol === 'https:';
const mod = isHttps ? https : http;
const bodyObj: Record<string, unknown> = {
model: config.model,
messages: messages.map(m => {
const msg: Record<string, unknown> = { role: m.role, content: m.content };
if (m.toolCalls && m.toolCalls.length > 0) {
msg.tool_calls = m.toolCalls.map(tc => ({
id: tc.id,
type: 'function',
function: { name: tc.name, arguments: tc.arguments }
}));
}
if (m.toolCallId) {
msg.tool_call_id = m.toolCallId;
}
if (m.role === 'assistant' && m.reasoningContent) {
msg.reasoning_content = m.reasoningContent;
}
return msg;
}),
max_tokens: config.maxTokens,
temperature: config.temperature,
stream: true
};
if (tools && tools.length > 0) {
bodyObj.tools = tools.map(t => ({
type: 'function',
function: { name: t.name, description: t.description, parameters: t.parameters }
}));
}
const body = JSON.stringify(bodyObj);
return new Promise<AIResponse>((resolve, reject) => {
const req = mod.request(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`,
'Accept': 'text/event-stream'
}
}, (res) => {
if (res.statusCode !== 200) {
let errBody = '';
res.on('data', (chunk: Buffer) => { errBody += chunk.toString(); });
res.on('end', () => reject(new Error(`API error ${res.statusCode}: ${errBody}`)));
return;
}
let fullContent = '';
let fullReasoningContent = '';
let buffer = '';
const toolCallMap = new Map<number, { id: string; name: string; arguments: string }>();
res.on('data', (chunk: Buffer) => {
buffer += chunk.toString();
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith('data: ')) continue;
const data = trimmed.slice(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const delta = parsed.choices?.[0]?.delta;
if (!delta) continue;
if (delta.content) {
fullContent += delta.content;
if (onChunk) onChunk(delta.content);
}
const rc = delta.reasoning_content || delta.reasoning ||
parsed.choices?.[0]?.reasoning_content || parsed.choices?.[0]?.reasoning;
if (rc) fullReasoningContent += rc;
if (delta.tool_calls) {
for (const tc of delta.tool_calls) {
const idx = tc.index;
if (!toolCallMap.has(idx)) {
toolCallMap.set(idx, { id: tc.id || '', name: '', arguments: '' });
}
const entry = toolCallMap.get(idx)!;
if (tc.id) entry.id = tc.id;
if (tc.function?.name) entry.name = tc.function.name;
if (tc.function?.arguments) entry.arguments += tc.function.arguments;
}
}
} catch { }
}
});
res.on('end', () => {
if (buffer.trim()) {
const trimmed = buffer.trim();
if (trimmed.startsWith('data: ') && trimmed.slice(6) !== '[DONE]') {
try {
const parsed = JSON.parse(trimmed.slice(6));
const delta = parsed.choices?.[0]?.delta;
if (delta?.content) {
fullContent += delta.content;
if (onChunk) onChunk(delta.content);
}
const rc2 = delta?.reasoning_content || delta?.reasoning ||
parsed.choices?.[0]?.reasoning_content || parsed.choices?.[0]?.reasoning;
if (rc2) fullReasoningContent += rc2;
if (delta?.tool_calls) {
for (const tc of delta.tool_calls) {
const idx = tc.index;
if (!toolCallMap.has(idx)) {
toolCallMap.set(idx, { id: tc.id || '', name: '', arguments: '' });
}
const entry = toolCallMap.get(idx)!;
if (tc.id) entry.id = tc.id;
if (tc.function?.name) entry.name = tc.function.name;
if (tc.function?.arguments) entry.arguments += tc.function.arguments;
}
}
} catch { }
}
}
const toolCalls: ToolCall[] = [];
for (const entry of toolCallMap.values()) {
toolCalls.push({ id: entry.id, name: entry.name, arguments: entry.arguments });
}
if (fullReasoningContent) {
console.log(`[VibeDev] Captured reasoning_content (len=${fullReasoningContent.length})`);
}
resolve({
content: fullContent,
toolCalls,
reasoningContent: fullReasoningContent || undefined
});
});
res.on('error', reject);
});
req.on('error', reject);
req.write(body);
req.end();
});
}
}