chatgpt-agent
Version:
Client for the unofficial ChatGPT API with progressive responses and more.
339 lines (329 loc) • 9.65 kB
JavaScript
// src/agent.ts
import EventEmitter3 from "node:events";
// src/session.ts
import EventEmitter2 from "node:events";
// src/conversation.ts
import EventEmitter from "node:events";
// src/request.ts
import { randomUUID } from "node:crypto";
import fetch, { Headers } from "node-fetch";
// src/debug.ts
import debug from "debug";
var log = debug("chatgpt");
// src/config.ts
var CHATGPT_BACKEND = process.env.CHATGPT_BACKEND || "https://chat.openai.com/backend-api";
var CHATGPT_TIMEOUT = Number(process.env.CHATGPT_TIMEOUT) || 6e4;
var CHATGPT_RETRY = Number(process.env.CHATGPT_RETRY) || 1;
// src/request.ts
function make_headers(token) {
const headers = new Headers();
if (token) {
headers.set("Authorization", `Bearer ${token}`);
}
headers.set("Content-Type", "application/json");
headers.set(
"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"
);
return headers;
}
async function moderate(token, input, backend = CHATGPT_BACKEND, timeout = CHATGPT_TIMEOUT) {
const headers = make_headers(token);
log(headers);
for (let i = 0; i <= CHATGPT_RETRY; i++) {
try {
const controller = new AbortController();
const timeout_id = setTimeout(() => controller.abort(), timeout);
const res = await fetch(`${backend}/moderations`, {
headers,
body: JSON.stringify({
input,
model: "text-moderation-playground"
}),
method: "POST",
signal: controller.signal
});
clearTimeout(timeout_id);
if (controller.signal.aborted) {
throw new Error("Request timed out");
}
log("sent moderation request", res.status);
if (res.status !== 200) {
try {
const data2 = await res.clone().json();
log("moderation error", data2);
throw new Error(data2?.error);
} catch {
const text = await res.clone().text();
log("moderation error", text);
throw new Error(text);
}
}
const data = await res.json();
return data;
} catch (err) {
if (i === CHATGPT_RETRY - 1) {
throw err;
}
log("retrying moderation request", i + 1);
}
}
throw new Error("Failed to moderate");
}
async function converse(token, content, conversation_id, parent_id, backend = CHATGPT_BACKEND, timeout = CHATGPT_TIMEOUT) {
const headers = make_headers(token);
headers.set("Accept", "text/event-stream");
log(headers);
for (let i = 0; i <= CHATGPT_RETRY; i++) {
try {
const controller = new AbortController();
const timeout_id = setTimeout(() => controller.abort(), timeout);
const res = await fetch(`${backend}/conversation`, {
headers,
body: JSON.stringify({
action: "next",
messages: [
{
id: randomUUID(),
role: "user",
content: { content_type: "text", parts: [content] }
}
],
parent_message_id: parent_id || randomUUID(),
conversation_id,
model: "text-davinci-002-render"
}),
method: "POST",
signal: controller.signal
});
clearTimeout(timeout_id);
if (controller.signal.aborted) {
throw new Error("Request timed out");
}
log("sent conversation request", res.status);
if (res.status !== 200) {
try {
const data = await res.clone().json();
log("conversation error", data);
throw new Error(data?.error?.detail);
} catch {
const text = await res.clone().text();
log("conversation error", text);
throw new Error(text);
}
}
return res.body;
} catch (err) {
if (i === CHATGPT_RETRY - 1) {
throw err;
}
log("retrying conversation request", i + 1);
}
}
throw new Error("Failed to converse");
}
async function refresh(refresh_token) {
const headers = make_headers();
headers.set("Cookie", `__Secure-next-auth.session-token=${refresh_token}`);
log(headers);
const res = await fetch("https://chat.openai.com/api/auth/session", { headers });
log("sent refresh request", res.status);
if (res.status !== 200) {
try {
const data2 = await res.clone().json();
log("refresh error", data2);
throw new Error(data2?.error);
} catch {
const text = await res.clone().text();
log("refresh error", text);
throw new Error(text);
}
}
const data = await res.json();
if (data?.error) {
throw new Error(data.error);
}
const new_refresh_token = res.headers.get("set-cookie")?.split(";")[0];
const refresh2 = new_refresh_token?.split("=")[1];
if (data?.accessToken) {
const new_refresh_token2 = res.headers.get("set-cookie")?.split(";")[0];
if (new_refresh_token2) {
log("refreshed refresh token", new_refresh_token2);
}
return {
token: data.accessToken,
refresh: refresh2 || ""
};
} else {
throw new Error("No access token returned");
}
}
// src/conversation.ts
var verbose = log.extend("verbose");
var Conversation = class extends EventEmitter {
constructor(session, message) {
super();
this.session = session;
this.message = message;
this.response = this.run();
}
async run() {
if (this.session.agent.validate() === false) {
try {
await this.session.agent.refresh();
} catch (err) {
this.emit("error", err);
}
}
const result = await this.converse().catch((err) => (this.emit("error", err), ""));
log("Conversation Result", result);
return result;
}
async converse() {
const history = this.session.history.filter((h) => h.author === "assistant");
const stream = await converse(
this.session.agent.token,
this.message,
this.session.id.startsWith("tmp-") ? void 0 : this.session.id,
history[history.length - 1]?.id,
this.session.agent.backend
);
let last;
return await new Promise((resolve) => {
let data = "";
stream.on("data", (chunk) => {
data += chunk.toString();
const split = data.indexOf("\n\n");
if (split !== -1) {
const part = data.slice(0, split).replace(/^data: /, "");
verbose("Partially Received", part);
if (part.startsWith("[DONE]")) {
return;
}
try {
const json = JSON.parse(part);
this.emit("partial", json.message.content.parts.join("\n"));
last = json;
} catch (err) {
this.emit("error", err);
}
data = data.slice(split + 2);
}
});
stream.on("end", () => {
const response = last?.message.content.parts.join("\n");
log("Received", response);
this.session.history.push({
id: last?.message.id,
author: "assistant",
message: response,
conversation: this
});
this.session.rename(last?.conversation_id);
this.emit("complete", response);
resolve(response);
});
});
}
on(event, listener) {
return super.on(event, listener);
}
once(event, listener) {
return super.once(event, listener);
}
off(event, listener) {
return super.off(event, listener);
}
emit(event, ...args) {
return super.emit(event, ...args);
}
};
// src/session.ts
var Session = class extends EventEmitter2 {
constructor(agent) {
super();
this.agent = agent;
this.id = `tmp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
this.history = [];
}
talk(message) {
this.history.push({
author: "user",
message,
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`
});
return new Conversation(this, message);
}
rename(id) {
this.emit("rename", { old: this.id, new: id });
this.id = id;
}
replies() {
return this.history.filter((h) => h.author === "assistant" && h.conversation).map((h) => h.conversation);
}
on(event, listener) {
return super.on(event, listener);
}
};
// src/agent.ts
var Agent = class extends EventEmitter3 {
constructor(token, refresh_token, {
backend = CHATGPT_BACKEND,
timeout = CHATGPT_TIMEOUT
} = {}) {
super();
this.token = token;
this.refresh_token = refresh_token;
this.sessions = /* @__PURE__ */ new Map();
this.backend = backend;
this.timeout = timeout;
}
session() {
const sess = new Session(this);
this.sessions.set(sess.id, sess);
sess.on("rename", (data) => {
this.sessions.delete(data.old);
this.sessions.set(data.new, sess);
});
return sess;
}
async refresh() {
if (!this.refresh_token) {
throw new Error("No refresh token");
}
const result = await refresh(this.refresh_token);
this.token = result.token;
this.refresh_token = result.refresh;
this.emit("refresh", result);
}
validate() {
try {
const [header, payload, signature] = this.token.split(".");
if (!header || !payload || !signature) {
return false;
}
const payloadJSON = JSON.parse(Buffer.from(payload, "base64").toString());
if (payloadJSON.exp < Date.now() / 1e3) {
return false;
}
return true;
} catch {
return false;
}
}
on(event, listener) {
return super.on(event, listener);
}
off(event, listener) {
return super.off(event, listener);
}
};
export {
Agent,
Conversation,
Session,
converse,
make_headers,
moderate,
refresh
};