import * as https from 'https';
import * as http from 'http';
import { ChatMessage, AIConfig, AIResponse, ToolDefinition, ToolCall } from '../common/types';
export class AntCompatClient {
async chat(
messages: ChatMessage[],
config: AIConfig,
tools?: ToolDefinition[],
onChunk?: (content: string) => void
): Promise<AIResponse> {
const url = new URL(`${config.baseUrl}/v1/messages`);
const isHttps = url.protocol === 'https:';
const mod = isHttps ? https : http;
const systemMsg = messages.find(m => m.role === 'system');
const chatMessages = messages
.filter(m => m.role !== 'system')
.map(m => {
if (m.role === 'tool') {
return {
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: m.toolCallId,
content: m.content
}]
};
}
if (m.toolCalls && m.toolCalls.length > 0) {
const content: Record<string, unknown>[] = [];
if (m.content) {
content.push({ type: 'text', text: m.content });
}
for (const tc of m.toolCalls) {
content.push({
type: 'tool_use',
id: tc.id,
name: tc.name,
input: JSON.parse(tc.arguments || '{}')
});
}
return { role: 'assistant', content };
}
return { role: m.role, content: m.content };
});
const bodyObj: Record<string, unknown> = {
model: config.model,
max_tokens: config.maxTokens,
temperature: config.temperature,
messages: chatMessages,
stream: true
};
if (systemMsg) {
bodyObj.system = systemMsg.content;
}
if (tools && tools.length > 0) {
bodyObj.tools = tools.map(t => ({
name: t.name,
description: t.description,
input_schema: 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',
'x-api-key': config.apiKey,
'anthropic-version': '2023-06-01',
'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 buffer = '';
const toolCalls: ToolCall[] = [];
let currentToolIndex = -1;
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;
let data: string;
try {
data = JSON.parse(trimmed.slice(6));
} catch { continue; }
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
if (parsed.type === 'content_block_start') {
if (parsed.content_block?.type === 'tool_use') {
currentToolIndex = toolCalls.length;
toolCalls.push({
id: parsed.content_block.id,
name: parsed.content_block.name,
arguments: ''
});
}
} else if (parsed.type === 'content_block_delta') {
if (parsed.delta?.type === 'text_delta' && parsed.delta.text) {
fullContent += parsed.delta.text;
if (onChunk) onChunk(parsed.delta.text);
} else if (parsed.delta?.type === 'input_json_delta' && parsed.delta.partial_json) {
if (currentToolIndex >= 0 && currentToolIndex < toolCalls.length) {
toolCalls[currentToolIndex].arguments += parsed.delta.partial_json;
}
}
} else if (parsed.type === 'content_block_stop') {
currentToolIndex = -1;
}
}
});
res.on('end', () => {
onChunk?.('');
resolve({ content: fullContent, toolCalls });
});
res.on('error', reject);
});
req.on('error', reject);
req.write(body);
req.end();
});
}
}