| | |
| | const BaseClient = require('./BaseClient'); |
| | const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken'); |
| | const Anthropic = require('@anthropic-ai/sdk'); |
| |
|
| | const HUMAN_PROMPT = '\n\nHuman:'; |
| | const AI_PROMPT = '\n\nAssistant:'; |
| |
|
| | const tokenizersCache = {}; |
| |
|
| | class AnthropicClient extends BaseClient { |
| | constructor(apiKey, options = {}, cacheOptions = {}) { |
| | super(apiKey, options, cacheOptions); |
| | this.apiKey = apiKey || process.env.ANTHROPIC_API_KEY; |
| | this.sender = 'Anthropic'; |
| | this.userLabel = HUMAN_PROMPT; |
| | this.assistantLabel = AI_PROMPT; |
| | this.setOptions(options); |
| | } |
| |
|
| | setOptions(options) { |
| | if (this.options && !this.options.replaceOptions) { |
| | |
| | this.options.modelOptions = { |
| | ...this.options.modelOptions, |
| | ...options.modelOptions, |
| | }; |
| | delete options.modelOptions; |
| | |
| | this.options = { |
| | ...this.options, |
| | ...options, |
| | }; |
| | } else { |
| | this.options = options; |
| | } |
| |
|
| | const modelOptions = this.options.modelOptions || {}; |
| | this.modelOptions = { |
| | ...modelOptions, |
| | |
| | model: modelOptions.model || 'claude-1', |
| | temperature: typeof modelOptions.temperature === 'undefined' ? 0.7 : modelOptions.temperature, |
| | topP: typeof modelOptions.topP === 'undefined' ? 0.7 : modelOptions.topP, |
| | topK: typeof modelOptions.topK === 'undefined' ? 40 : modelOptions.topK, |
| | stop: modelOptions.stop, |
| | }; |
| |
|
| | this.maxContextTokens = this.options.maxContextTokens || 99999; |
| | this.maxResponseTokens = this.modelOptions.maxOutputTokens || 1500; |
| | this.maxPromptTokens = |
| | this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens; |
| |
|
| | if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) { |
| | throw new Error( |
| | `maxPromptTokens + maxOutputTokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${ |
| | this.maxPromptTokens + this.maxResponseTokens |
| | }) must be less than or equal to maxContextTokens (${this.maxContextTokens})`, |
| | ); |
| | } |
| |
|
| | this.startToken = '||>'; |
| | this.endToken = ''; |
| | this.gptEncoder = this.constructor.getTokenizer('cl100k_base'); |
| |
|
| | if (!this.modelOptions.stop) { |
| | const stopTokens = [this.startToken]; |
| | if (this.endToken && this.endToken !== this.startToken) { |
| | stopTokens.push(this.endToken); |
| | } |
| | stopTokens.push(`${this.userLabel}`); |
| | stopTokens.push('<|diff_marker|>'); |
| |
|
| | this.modelOptions.stop = stopTokens; |
| | } |
| |
|
| | return this; |
| | } |
| |
|
| | getClient() { |
| | if (this.options.reverseProxyUrl) { |
| | return new Anthropic({ |
| | apiKey: this.apiKey, |
| | baseURL: this.options.reverseProxyUrl, |
| | }); |
| | } else { |
| | return new Anthropic({ |
| | apiKey: this.apiKey, |
| | }); |
| | } |
| | } |
| |
|
| | async buildMessages(messages, parentMessageId) { |
| | const orderedMessages = this.constructor.getMessagesForConversation({ |
| | messages, |
| | parentMessageId, |
| | }); |
| | if (this.options.debug) { |
| | console.debug('AnthropicClient: orderedMessages', orderedMessages, parentMessageId); |
| | } |
| |
|
| | const formattedMessages = orderedMessages.map((message) => ({ |
| | author: message.isCreatedByUser ? this.userLabel : this.assistantLabel, |
| | content: message?.content ?? message.text, |
| | })); |
| |
|
| | let lastAuthor = ''; |
| | let groupedMessages = []; |
| |
|
| | for (let message of formattedMessages) { |
| | |
| | if (lastAuthor !== message.author) { |
| | groupedMessages.push({ |
| | author: message.author, |
| | content: [message.content], |
| | }); |
| | lastAuthor = message.author; |
| | |
| | } else { |
| | groupedMessages[groupedMessages.length - 1].content.push(message.content); |
| | } |
| | } |
| |
|
| | let identityPrefix = ''; |
| | if (this.options.userLabel) { |
| | identityPrefix = `\nHuman's name: ${this.options.userLabel}`; |
| | } |
| |
|
| | if (this.options.modelLabel) { |
| | identityPrefix = `${identityPrefix}\nYou are ${this.options.modelLabel}`; |
| | } |
| |
|
| | let promptPrefix = (this.options.promptPrefix || '').trim(); |
| | if (promptPrefix) { |
| | |
| | if (!promptPrefix.endsWith(`${this.endToken}`)) { |
| | promptPrefix = `${promptPrefix.trim()}${this.endToken}\n\n`; |
| | } |
| | promptPrefix = `\nContext:\n${promptPrefix}`; |
| | } |
| |
|
| | if (identityPrefix) { |
| | promptPrefix = `${identityPrefix}${promptPrefix}`; |
| | } |
| |
|
| | |
| | let isEdited = lastAuthor === this.assistantLabel; |
| | const promptSuffix = isEdited ? '' : `${promptPrefix}${this.assistantLabel}\n`; |
| | let currentTokenCount = isEdited |
| | ? this.getTokenCount(promptPrefix) |
| | : this.getTokenCount(promptSuffix); |
| |
|
| | let promptBody = ''; |
| | const maxTokenCount = this.maxPromptTokens; |
| |
|
| | const context = []; |
| |
|
| | |
| | |
| | |
| | |
| | const nextMessage = { |
| | remove: false, |
| | tokenCount: 0, |
| | messageString: '', |
| | }; |
| |
|
| | const buildPromptBody = async () => { |
| | if (currentTokenCount < maxTokenCount && groupedMessages.length > 0) { |
| | const message = groupedMessages.pop(); |
| | const isCreatedByUser = message.author === this.userLabel; |
| | |
| | const messagePrefix = |
| | isCreatedByUser || !isEdited ? message.author : `${promptPrefix}${message.author}`; |
| | const messageString = `${messagePrefix}\n${message.content}${this.endToken}\n`; |
| | let newPromptBody = `${messageString}${promptBody}`; |
| |
|
| | context.unshift(message); |
| |
|
| | const tokenCountForMessage = this.getTokenCount(messageString); |
| | const newTokenCount = currentTokenCount + tokenCountForMessage; |
| |
|
| | if (!isCreatedByUser) { |
| | nextMessage.messageString = messageString; |
| | nextMessage.tokenCount = tokenCountForMessage; |
| | } |
| |
|
| | if (newTokenCount > maxTokenCount) { |
| | if (!promptBody) { |
| | |
| | throw new Error( |
| | `Prompt is too long. Max token count is ${maxTokenCount}, but prompt is ${newTokenCount} tokens long.`, |
| | ); |
| | } |
| |
|
| | |
| | |
| | if (isCreatedByUser) { |
| | nextMessage.remove = true; |
| | } |
| |
|
| | return false; |
| | } |
| | promptBody = newPromptBody; |
| | currentTokenCount = newTokenCount; |
| |
|
| | |
| | if (isEdited) { |
| | isEdited = false; |
| | } |
| |
|
| | |
| | await new Promise((resolve) => setImmediate(resolve)); |
| | return buildPromptBody(); |
| | } |
| | return true; |
| | }; |
| |
|
| | await buildPromptBody(); |
| |
|
| | if (nextMessage.remove) { |
| | promptBody = promptBody.replace(nextMessage.messageString, ''); |
| | currentTokenCount -= nextMessage.tokenCount; |
| | context.shift(); |
| | } |
| |
|
| | let prompt = `${promptBody}${promptSuffix}`; |
| |
|
| | |
| | currentTokenCount += 2; |
| |
|
| | |
| | this.modelOptions.maxOutputTokens = Math.min( |
| | this.maxContextTokens - currentTokenCount, |
| | this.maxResponseTokens, |
| | ); |
| |
|
| | return { prompt, context }; |
| | } |
| |
|
| | getCompletion() { |
| | console.log('AnthropicClient doesn\'t use getCompletion (all handled in sendCompletion)'); |
| | } |
| |
|
| | async sendCompletion(payload, { onProgress, abortController }) { |
| | if (!abortController) { |
| | abortController = new AbortController(); |
| | } |
| |
|
| | const { signal } = abortController; |
| |
|
| | const modelOptions = { ...this.modelOptions }; |
| | if (typeof onProgress === 'function') { |
| | modelOptions.stream = true; |
| | } |
| |
|
| | const { debug } = this.options; |
| | if (debug) { |
| | console.debug(); |
| | console.debug(modelOptions); |
| | console.debug(); |
| | } |
| |
|
| | const client = this.getClient(); |
| | const metadata = { |
| | user_id: this.user, |
| | }; |
| |
|
| | let text = ''; |
| | const { |
| | stream, |
| | model, |
| | temperature, |
| | maxOutputTokens, |
| | stop: stop_sequences, |
| | topP: top_p, |
| | topK: top_k, |
| | } = this.modelOptions; |
| | const requestOptions = { |
| | prompt: payload, |
| | model, |
| | stream: stream || true, |
| | max_tokens_to_sample: maxOutputTokens || 1500, |
| | stop_sequences, |
| | temperature, |
| | metadata, |
| | top_p, |
| | top_k, |
| | }; |
| | if (this.options.debug) { |
| | console.log('AnthropicClient: requestOptions'); |
| | console.dir(requestOptions, { depth: null }); |
| | } |
| | const response = await client.completions.create(requestOptions); |
| |
|
| | signal.addEventListener('abort', () => { |
| | if (this.options.debug) { |
| | console.log('AnthropicClient: message aborted!'); |
| | } |
| | response.controller.abort(); |
| | }); |
| |
|
| | for await (const completion of response) { |
| | if (this.options.debug) { |
| | |
| | |
| | } |
| | text += completion.completion; |
| | onProgress(completion.completion); |
| | } |
| |
|
| | signal.removeEventListener('abort', () => { |
| | if (this.options.debug) { |
| | console.log('AnthropicClient: message aborted!'); |
| | } |
| | response.controller.abort(); |
| | }); |
| |
|
| | return text.trim(); |
| | } |
| |
|
| | getSaveOptions() { |
| | return { |
| | promptPrefix: this.options.promptPrefix, |
| | modelLabel: this.options.modelLabel, |
| | ...this.modelOptions, |
| | }; |
| | } |
| |
|
| | getBuildMessagesOptions() { |
| | if (this.options.debug) { |
| | console.log('AnthropicClient doesn\'t use getBuildMessagesOptions'); |
| | } |
| | } |
| |
|
| | static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) { |
| | if (tokenizersCache[encoding]) { |
| | return tokenizersCache[encoding]; |
| | } |
| | let tokenizer; |
| | if (isModelName) { |
| | tokenizer = encodingForModel(encoding, extendSpecialTokens); |
| | } else { |
| | tokenizer = getEncoding(encoding, extendSpecialTokens); |
| | } |
| | tokenizersCache[encoding] = tokenizer; |
| | return tokenizer; |
| | } |
| |
|
| | getTokenCount(text) { |
| | return this.gptEncoder.encode(text, 'all').length; |
| | } |
| | } |
| |
|
| | module.exports = AnthropicClient; |
| |
|