Files
KGStudio/src/agent/llm/ClaudeProvider.ts
T
2025-08-16 16:25:45 -07:00

162 lines
4.2 KiB
TypeScript

import { LLMProvider } from './LLMProvider';
import type { StreamChunk } from './StreamingTypes';
import type { Message } from '../core/AgentState';
import { ConfigManager } from '../../core/config/ConfigManager';
/**
* Anthropic Claude API provider implementation
*/
export class ClaudeProvider extends LLMProvider {
readonly name = 'Claude';
private apiKey: string;
private model: string;
private baseURL: string = 'https://api.anthropic.com';
private apiEndpoint: string;
constructor() {
super();
const configManager = ConfigManager.instance();
this.apiKey = configManager.get('general.claude.api_key') as string;
this.model = configManager.get('general.claude.model') as string;
this.apiEndpoint = `${this.baseURL}/v1/messages`;
}
/**
* Convert internal messages to Claude's format
*/
private convertMessages(messages: Message[], systemPrompt?: string): {
system?: string;
messages: Array<{ role: 'user' | 'assistant'; content: string }>;
} {
const claudeMessages: Array<{ role: 'user' | 'assistant'; content: string }> = [];
claudeMessages.push(...messages.map(msg => ({ role: msg.role, content: msg.content })));
return {
system: systemPrompt,
messages: claudeMessages
};
}
/**
* Parse Claude's streaming response chunks
*/
private parseClaudeStreamChunk(line: string): { content?: string; isDone?: boolean } {
if (!line.startsWith('data: ')) {
return {};
}
const data = line.slice(6);
if (data === '[DONE]') {
return { isDone: true };
}
try {
const json = JSON.parse(data);
// Handle different Claude streaming event types
switch (json.type) {
case 'content_block_delta':
return {
content: json.delta?.text,
isDone: false
};
case 'message_stop':
return { isDone: true };
default:
return {};
}
} catch {
return {}; // Skip invalid JSON lines
}
}
async *generateStream(
messages: Message[],
systemPrompt?: string,
tools?: Record<string, unknown>[]
): AsyncIterableIterator<StreamChunk> {
const { system, messages: claudeMessages } = this.convertMessages(messages, systemPrompt);
const requestBody: {
model: string;
max_tokens: number;
messages: Array<{ role: 'user' | 'assistant'; content: string }>;
stream: boolean;
system?: string;
tools?: Record<string, unknown>[];
} = {
model: this.model,
max_tokens: 8192,
messages: claudeMessages,
stream: true
};
if (system) {
requestBody.system = system;
}
if (tools && tools.length > 0) {
requestBody.tools = tools;
}
const response = await fetch(this.apiEndpoint, {
method: 'POST',
headers: {
'x-api-key': this.apiKey,
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01'
},
body: JSON.stringify(requestBody),
mode: 'cors'
});
if (!response.ok) {
throw new Error(`Claude API error: ${response.status} ${response.statusText}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('Failed to get response reader');
}
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine) continue;
const parseResult = this.parseClaudeStreamChunk(trimmedLine);
if (parseResult.isDone) {
yield { type: 'done', content: '' };
return;
}
if (parseResult.content) {
yield {
type: 'text',
content: parseResult.content
};
}
}
}
} finally {
reader.releaseLock();
}
}
}