chatgpt-plugin/client/ClaudeAPIClient.js

195 lines
5.1 KiB
JavaScript

import crypto from 'crypto'
import { newFetch } from '../utils/proxy.js'
import _ from 'lodash'
import { getMessageById, upsertMessage } from '../utils/history.js'
import { BaseClient } from './BaseClient.js'
const BASEURL = 'https://api.anthropic.com'
/**
* @typedef {Object} Content
* @property {string} model
* @property {string} system
* @property {number} max_tokens
* @property {boolean} stream
* @property {Array<{
* role: 'user'|'assistant',
* content: string|Array<{
* type: 'text'|'image',
* text?: string,
* source?: {
* type: 'base64',
* media_type: 'image/jpeg'|'image/png'|'image/gif'|'image/webp',
* data: string
* }
* }>
* }>} messages
*
* Claude消息的基本格式
*/
/**
* @typedef {Object} ClaudeResponse
* @property {string} id
* @property {string} type
* @property {number} role
* @property {number} model
* @property {number} stop_reason
* @property {number} stop_sequence
* @property {number} role
* @property {boolean} stream
* @property {Array<{
* type: string,
* text: string
* }>} content
* @property {Array<{
* input_tokens: number,
* output_tokens: number,
* }>} usage
* @property {{
* type: string,
* message: string,
* }} error
* Claude响应的基本格式
*/
export class ClaudeAPIClient extends BaseClient {
constructor (props) {
if (!props.upsertMessage) {
props.upsertMessage = async function umGemini (message) {
return await upsertMessage(message, 'Claude')
}
}
if (!props.getMessageById) {
props.getMessageById = async function umGemini (message) {
return await getMessageById(message, 'Claude')
}
}
super(props)
this.model = props.model
this.key = props.key
if (!this.key) {
throw new Error('no claude API key')
}
this.baseUrl = props.baseUrl || BASEURL
this.supportFunction = false
this.debug = props.debug
}
async getHistory (parentMessageId, userId = this.userId, opt = {}) {
const history = []
let cursor = parentMessageId
if (!cursor) {
return history
}
do {
let parentMessage = await this.getMessageById(cursor)
if (!parentMessage) {
break
} else {
history.push(parentMessage)
cursor = parentMessage.parentMessageId
if (!cursor) {
break
}
}
} while (true)
return history.reverse()
}
/**
*
* @param text
* @param {{conversationId: string?, parentMessageId: string?, stream: boolean?, onProgress: function?, functionResponse: FunctionResponse?, system: string?, image: string?, model: string?}} opt
* @returns {Promise<{conversationId: string?, parentMessageId: string, text: string, id: string}>}
*/
async sendMessage (text, opt = {}) {
let history = await this.getHistory(opt.parentMessageId)
/**
* 发送的body
* @type {Content}
* @see https://docs.anthropic.com/claude/reference/messages_post
*/
let body = {}
if (opt.system) {
body.system = opt.system
}
const idThis = crypto.randomUUID()
const idModel = crypto.randomUUID()
/**
* @type {Array<{
* role: 'user'|'assistant',
* content: string|Array<{
* type: 'text'|'image',
* text?: string,
* source?: {
* type: 'base64',
* media_type: 'image/jpeg'|'image/png'|'image/gif'|'image/webp',
* data: string
* }
* }>
* }>}
*/
let thisContent = [{ type: 'text', text }]
if (opt.image) {
thisContent.push({
type: 'image',
source: {
type: 'base64',
media_type: 'image/jpeg',
data: opt.image
}
})
}
const thisMessage = {
role: 'user',
content: thisContent,
id: idThis,
parentMessageId: opt.parentMessageId || undefined
}
history.push(_.cloneDeep(thisMessage))
let messages = history.map(h => { return { role: h.role, content: h.content } })
body = Object.assign(body, {
model: opt.model || this.model || 'claude-3-opus-20240229',
max_tokens: opt.max_tokens || 1024,
messages,
stream: false
})
let url = `${this.baseUrl}/v1/messages`
let result = await newFetch(url, {
headers: {
'anthropic-version': '2023-06-01',
'x-api-key': this.key,
'content-type': 'application/json'
},
method: 'POST',
body: JSON.stringify(body)
})
if (result.status !== 200) {
throw new Error(await result.text())
}
/**
* @type {ClaudeResponse}
*/
let response = await result.json()
if (this.debug) {
console.log(JSON.stringify(response))
}
if (response.type === 'error') {
logger.error(response.error.message)
throw new Error(response.error.type)
}
await this.upsertMessage(thisMessage)
const respMessage = Object.assign(response, {
id: idModel,
parentMessageId: idThis
})
await this.upsertMessage(respMessage)
return {
text: response.content[0].text,
conversationId: '',
parentMessageId: idThis,
id: idModel
}
}
}