chatgpt-api/src/chatgpt-api.ts

278 wiersze
8.3 KiB
TypeScript
Czysty Zwykły widok Historia

2022-12-05 05:13:36 +00:00
import ExpiryMap from 'expiry-map'
import { v4 as uuidv4 } from 'uuid'
import * as types from './types'
import { fetch } from './fetch'
import { fetchSSE } from './fetch-sse'
2022-12-05 05:13:36 +00:00
import { markdownToText } from './utils'
const KEY_ACCESS_TOKEN = 'accessToken'
const USER_AGENT =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36'
2022-12-02 23:43:59 +00:00
2022-12-06 07:38:32 +00:00
class Conversation {
protected _api: ChatGPTAPI
protected _conversationId: string = undefined
protected _parentMessageId: string = undefined
constructor(api: ChatGPTAPI) {
this._api = api
}
/**
* Sends a message to ChatGPT, waits for the response to resolve, and returns
* the response. If the conversation has not yet been started, this will
* automatically start the conversation.
* @param message - The plaintext message to send.
* @param opts.onProgress - Optional listener which will be called every time the partial response is updated
* @param opts.onConversationResponse - Optional listener which will be called every time a conversation response is received
* @returns The plaintext response from ChatGPT.
*/
async sendMessage(
message: string,
opts: {
onProgress?: (partialResponse: string) => void
onConversationResponse?: (
response: types.ConversationResponseEvent
) => void
} = {}
) {
const { onProgress, onConversationResponse } = opts
if (!this._conversationId) {
return this._api.sendMessage(message, {
onProgress,
onConversationResponse: (response) => {
this._conversationId = response.conversation_id
this._parentMessageId = response.message.id
onConversationResponse?.(response)
}
})
}
return this._api.sendMessage(message, {
conversationId: this._conversationId,
parentMessageId: this._parentMessageId,
onProgress,
onConversationResponse: (response) => {
this._parentMessageId = response.message.id
onConversationResponse?.(response)
}
})
}
}
2022-12-02 23:43:59 +00:00
export class ChatGPTAPI {
2022-12-05 05:13:36 +00:00
protected _sessionToken: string
2022-12-02 23:43:59 +00:00
protected _markdown: boolean
2022-12-05 05:13:36 +00:00
protected _apiBaseUrl: string
protected _backendApiBaseUrl: string
protected _userAgent: string
2022-12-05 05:34:15 +00:00
// stores access tokens for up to 10 seconds before needing to refresh
2022-12-05 05:13:36 +00:00
protected _accessTokenCache = new ExpiryMap<string, string>(10 * 1000)
2022-12-02 23:43:59 +00:00
/**
2022-12-05 05:13:36 +00:00
* Creates a new client wrapper around the unofficial ChatGPT REST API.
*
* @param opts.sessionToken = **Required** OpenAI session token which can be found in a valid session's cookies (see readme for instructions)
* @param apiBaseUrl - Optional override; the base URL for ChatGPT webapp's API (`/api`)
* @param backendApiBaseUrl - Optional override; the base URL for the ChatGPT backend API (`/backend-api`)
* @param userAgent - Optional override; the `user-agent` header to use with ChatGPT requests
2022-12-02 23:43:59 +00:00
*/
2022-12-05 05:13:36 +00:00
constructor(opts: {
sessionToken: string
2022-12-02 23:43:59 +00:00
2022-12-05 05:13:36 +00:00
/** @defaultValue `true` **/
markdown?: boolean
2022-12-02 23:43:59 +00:00
2022-12-05 05:13:36 +00:00
/** @defaultValue `'https://chat.openai.com/api'` **/
apiBaseUrl?: string
2022-12-02 23:43:59 +00:00
2022-12-05 05:13:36 +00:00
/** @defaultValue `'https://chat.openai.com/backend-api'` **/
backendApiBaseUrl?: string
/** @defaultValue `'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36'` **/
userAgent?: string
}) {
2022-12-02 23:43:59 +00:00
const {
2022-12-05 05:13:36 +00:00
sessionToken,
markdown = true,
apiBaseUrl = 'https://chat.openai.com/api',
backendApiBaseUrl = 'https://chat.openai.com/backend-api',
userAgent = USER_AGENT
2022-12-02 23:43:59 +00:00
} = opts
2022-12-05 05:13:36 +00:00
this._sessionToken = sessionToken
2022-12-02 23:43:59 +00:00
this._markdown = !!markdown
2022-12-05 05:13:36 +00:00
this._apiBaseUrl = apiBaseUrl
this._backendApiBaseUrl = backendApiBaseUrl
this._userAgent = userAgent
2022-12-03 00:04:53 +00:00
2022-12-05 05:13:36 +00:00
if (!this._sessionToken) {
throw new Error('ChatGPT invalid session token')
2022-12-03 00:04:53 +00:00
}
2022-12-02 23:43:59 +00:00
}
2022-12-05 05:13:36 +00:00
async getIsAuthenticated() {
2022-12-03 08:46:57 +00:00
try {
2022-12-05 05:13:36 +00:00
void (await this.refreshAccessToken())
return true
2022-12-03 08:46:57 +00:00
} catch (err) {
return false
}
2022-12-02 23:43:59 +00:00
}
2022-12-05 05:13:36 +00:00
async ensureAuth() {
return await this.refreshAccessToken()
2022-12-02 23:43:59 +00:00
}
2022-12-05 05:13:36 +00:00
/**
* Sends a message to ChatGPT, waits for the response to resolve, and returns
* the response.
*
* @param message - The plaintext message to send.
* @param opts.conversationId - Optional ID of the previous message in a conversation
* @param opts.onProgress - Optional listener which will be called every time the partial response is updated
2022-12-06 07:38:32 +00:00
* @param opts.onConversationResponse - Optional listener which will be called every time the partial response is updated with the full conversation response
2022-12-05 05:13:36 +00:00
*/
async sendMessage(
message: string,
opts: {
conversationId?: string
2022-12-06 07:38:32 +00:00
parentMessageId?: string
2022-12-05 05:13:36 +00:00
onProgress?: (partialResponse: string) => void
2022-12-06 07:38:32 +00:00
onConversationResponse?: (
response: types.ConversationResponseEvent
) => void
2022-12-05 05:13:36 +00:00
} = {}
): Promise<string> {
2022-12-06 07:38:32 +00:00
const {
conversationId,
parentMessageId = uuidv4(),
onProgress,
onConversationResponse
} = opts
2022-12-05 05:13:36 +00:00
const accessToken = await this.refreshAccessToken()
const body: types.ConversationJSONBody = {
action: 'next',
messages: [
{
id: uuidv4(),
role: 'user',
content: {
content_type: 'text',
parts: [message]
}
}
],
model: 'text-davinci-002-render',
2022-12-06 07:38:32 +00:00
parent_message_id: parentMessageId
}
if (conversationId) {
body.conversation_id = conversationId
2022-12-05 05:13:36 +00:00
}
2022-12-02 23:43:59 +00:00
2022-12-05 05:13:36 +00:00
const url = `${this._backendApiBaseUrl}/conversation`
// TODO: What's the best way to differentiate btwn wanting just the response text
// versus wanting the full response message, so you can extract the ID and other
// metadata?
// let fullResponse: types.Message = null
let response = ''
return new Promise((resolve, reject) => {
fetchSSE(url, {
2022-12-05 05:13:36 +00:00
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'user-agent': this._userAgent
},
body: JSON.stringify(body),
onMessage: (data: string) => {
if (data === '[DONE]') {
return resolve(response)
}
try {
const parsedData: types.ConversationResponseEvent = JSON.parse(data)
2022-12-06 07:38:32 +00:00
if (onConversationResponse) {
onConversationResponse(parsedData)
}
2022-12-05 05:13:36 +00:00
const message = parsedData.message
2022-12-05 05:34:15 +00:00
// console.log('event', JSON.stringify(parsedData, null, 2))
2022-12-05 05:13:36 +00:00
if (message) {
let text = message?.content?.parts?.[0]
if (text) {
if (!this._markdown) {
text = markdownToText(text)
}
response = text
2022-12-05 05:34:15 +00:00
// fullResponse = message
2022-12-05 05:13:36 +00:00
if (onProgress) {
onProgress(text)
}
}
}
} catch (err) {
console.warn('fetchSSE onMessage unexpected error', err)
reject(err)
}
}
}).catch(reject)
})
2022-12-02 23:43:59 +00:00
}
2022-12-05 05:13:36 +00:00
async refreshAccessToken(): Promise<string> {
const cachedAccessToken = this._accessTokenCache.get(KEY_ACCESS_TOKEN)
if (cachedAccessToken) {
return cachedAccessToken
2022-12-02 23:43:59 +00:00
}
2022-12-05 05:13:36 +00:00
try {
const res = await fetch('https://chat.openai.com/api/auth/session', {
headers: {
cookie: `__Secure-next-auth.session-token=${this._sessionToken}`,
'user-agent': this._userAgent
}
}).then((r) => r.json() as any as types.SessionResult)
2022-12-02 23:43:59 +00:00
2022-12-05 05:13:36 +00:00
const accessToken = res?.accessToken
2022-12-02 23:43:59 +00:00
2022-12-05 05:13:36 +00:00
if (!accessToken) {
throw new Error('Unauthorized')
2022-12-02 23:43:59 +00:00
}
const error = res?.error
if (error) {
if (error === 'RefreshAccessTokenError') {
throw new Error('session token has expired')
} else {
throw new Error(error)
}
}
2022-12-05 05:13:36 +00:00
this._accessTokenCache.set(KEY_ACCESS_TOKEN, accessToken)
return accessToken
} catch (err: any) {
throw new Error(`ChatGPT failed to refresh auth token. ${err.toString()}`)
2022-12-05 05:13:36 +00:00
}
2022-12-05 05:14:23 +00:00
}
2022-12-06 07:38:32 +00:00
/**
* Get a new Conversation instance, which can be used to send multiple messages as part of a single conversation.
*
* @returns a new Conversation instance
*/
getConversation() {
return new Conversation(this)
}
2022-12-02 23:43:59 +00:00
}