chatgpt-agent
Version:
Client for the unofficial ChatGPT API with progressive responses and more.
378 lines (366 loc) • 11.4 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
Agent: () => Agent,
Conversation: () => Conversation,
Session: () => Session,
converse: () => converse,
make_headers: () => make_headers,
moderate: () => moderate,
refresh: () => refresh
});
module.exports = __toCommonJS(src_exports);
// src/agent.ts
var import_node_events3 = __toESM(require("events"));
// src/session.ts
var import_node_events2 = __toESM(require("events"));
// src/conversation.ts
var import_node_events = __toESM(require("events"));
// src/request.ts
var import_node_crypto = require("crypto");
var import_node_fetch = __toESM(require("node-fetch"));
// src/debug.ts
var import_debug = __toESM(require("debug"));
var log = (0, import_debug.default)("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 import_node_fetch.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 (0, import_node_fetch.default)(`${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 (0, import_node_fetch.default)(`${backend}/conversation`, {
headers,
body: JSON.stringify({
action: "next",
messages: [
{
id: (0, import_node_crypto.randomUUID)(),
role: "user",
content: { content_type: "text", parts: [content] }
}
],
parent_message_id: parent_id || (0, import_node_crypto.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 (0, import_node_fetch.default)("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 import_node_events.default {
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 import_node_events2.default {
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 import_node_events3.default {
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);
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Agent,
Conversation,
Session,
converse,
make_headers,
moderate,
refresh
});