Skip to content

Commit 6bbf689

Browse files
authored
Merge pull request #5 from gin337/beta
Beta
2 parents 7dabded + d2cd93b commit 6bbf689

7 files changed

Lines changed: 204 additions & 38 deletions

File tree

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- name: Set up Node
1717
uses: actions/setup-node@v4
1818
with:
19-
node-version: 20
19+
node-version: 24
2020
registry-url: "https://registry.npmjs.org/"
2121

2222
- name: Install dependencies

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "chatgptreversed",
3-
"version": "0.2.4",
3+
"version": "0.2.5",
44
"main": "./dist/index.js",
55
"module": "./dist/index.mjs",
66
"types": "./dist/index.d.ts",

src/index.ts

Lines changed: 178 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {randomUUID} from "crypto";
2-
import {
3-
generateFakeSentinelToken,
4-
simulateBypassHeaders,
5-
solveSentinelChallenge,
6-
} from "./utils/utils";
2+
import {generateFakeSentinelToken, simulateBypassHeaders, solveSentinelChallenge} from "./utils/utils";
3+
4+
interface CompleteOptionsProfile {
5+
stream?: boolean;
6+
}
77

88
export class ChatGPTReversed {
99
public static csrfToken: string | undefined = undefined;
@@ -114,7 +114,17 @@ export class ChatGPTReversed {
114114
};
115115
}
116116

117-
public async complete(message: string): Promise<string> {
117+
public async complete(
118+
message: string,
119+
options: {stream: true}
120+
): Promise<AsyncGenerator<{text: string; metadata: any}>>;
121+
122+
public async complete(message: string, options?: {stream?: false}): Promise<string>;
123+
124+
public async complete(
125+
message: string,
126+
options?: CompleteOptionsProfile
127+
): Promise<string | AsyncGenerator<{text: string; metadata: any}>> {
118128
const sessionData = await this.rotateSessionData();
119129

120130
if (!ChatGPTReversed.initialized) {
@@ -129,6 +139,8 @@ export class ChatGPTReversed {
129139
preOaiUUID: sessionData.uuid,
130140
});
131141

142+
const messageID = randomUUID();
143+
132144
const response = await fetch("https://chatgpt.com/backend-anon/conversation", {
133145
headers: {
134146
...headers,
@@ -140,79 +152,213 @@ export class ChatGPTReversed {
140152
action: "next",
141153
messages: [
142154
{
143-
id: randomUUID(),
155+
id: messageID,
144156
author: {
145157
role: "user",
146158
},
159+
create_time: Date.now(),
147160
content: {
148161
content_type: "text",
149162
parts: [message],
150163
},
151-
metadata: {},
164+
metadata: {
165+
selected_all_github_repos: false,
166+
selected_github_repos: [],
167+
serialization_metadata: {
168+
custom_symbol_offsets: [],
169+
},
170+
dictation: false,
171+
},
152172
},
153173
],
154-
parent_message_id: randomUUID(),
174+
paragen_cot_summary_display_override: "allow",
175+
parent_message_id: "client-created-root",
155176
model: "auto",
156-
timezone_offset_min: -120,
177+
timezone_offset_min: -60,
178+
timezone: "Europe/Berlin",
157179
suggestions: [],
158-
history_and_training_disabled: false,
180+
history_and_training_disabled: true,
159181
conversation_mode: {
160182
kind: "primary_assistant",
161-
plugin_ids: null,
162183
},
163-
force_paragen: false,
164-
force_paragen_model_slug: "",
165-
force_nulligen: false,
166-
force_rate_limit: false,
167-
reset_rate_limits: false,
168-
websocket_request_id: randomUUID(),
169-
force_use_sse: true,
184+
system_hints: [],
185+
supports_buffering: true,
186+
supported_encodings: ["v1"],
187+
client_contextual_info: {
188+
is_dark_mode: true,
189+
time_since_loaded: 7,
190+
page_height: 911,
191+
page_width: 1080,
192+
pixel_ratio: 1,
193+
screen_height: 1080,
194+
screen_width: 1920,
195+
app_name: "chatgpt.com",
196+
},
170197
}),
171198
method: "POST",
172199
});
173200

201+
if (!response.ok) {
202+
throw new Error(`Request failed with status ${response.status}: ${response.statusText}`);
203+
}
204+
174205
if (response.body === null) {
175206
throw new Error("Failed to receive response body. Please check your sessionToken and try again.");
176207
}
177208

178-
const reader = response.body.getReader();
209+
if (options?.stream) {
210+
return this.streamResponse(response);
211+
}
212+
213+
return this.collectFullResponse(response);
214+
}
215+
216+
private async collectFullResponse(response: Response): Promise<string> {
217+
const reader = response.body!.getReader();
179218
const decoder = new TextDecoder();
219+
180220
let result = "";
181-
let buffer: any = "";
221+
let buffer = "";
222+
let finished = false;
182223

183224
while (true) {
184225
const {done, value} = await reader.read();
185226
if (done) break;
186227

187228
buffer += decoder.decode(value, {stream: true});
188229
let lines = buffer.split("\n");
189-
buffer = lines.pop();
230+
buffer = lines.pop() || "";
190231

191232
for (const line of lines) {
192-
if (line.startsWith("data:")) {
193-
try {
194-
const json = JSON.parse(line.replace("data:", "").trim());
195-
if (json.message?.content?.parts && json.message.status === "finished_successfully") {
233+
if (!line.startsWith("data:")) continue;
234+
const dataStr = line.replace("data:", "").trim();
235+
if (!dataStr || dataStr === "[DONE]") continue;
236+
237+
try {
238+
const json = JSON.parse(dataStr);
239+
240+
if (json.message) {
241+
if (json.message.content && json.message.content.parts) {
196242
result = json.message.content.parts[0];
243+
}
244+
if (json.message.status === "finished_successfully") {
245+
finished = true;
197246
break;
198247
}
199-
} catch (e) {
200-
continue;
248+
} else if (json.o === "append" && json.p === "/message/content/parts/0") {
249+
result += json.v;
250+
} else if (Array.isArray(json.v)) {
251+
for (const op of json.v) {
252+
if (op.o === "append" && op.p === "/message/content/parts/0") {
253+
result += op.v;
254+
}
255+
if (op.p === "/message/status" && op.o === "replace" && op.v === "finished_successfully") {
256+
finished = true;
257+
}
258+
}
201259
}
260+
} catch {
261+
continue;
202262
}
203263
}
204-
if (result) break;
264+
265+
if (finished) break;
205266
}
206267

207-
if (!result && buffer.startsWith("data:")) {
268+
if (!finished && buffer.startsWith("data:")) {
208269
try {
209270
const json = JSON.parse(buffer.replace("data:", "").trim());
210-
if (json.message?.content?.parts && json.message.status === "finished_successfully") {
271+
if (json.message && json.message.content && json.message.content.parts) {
211272
result = json.message.content.parts[0];
212273
}
213-
} catch (e) {}
274+
} catch {
275+
}
214276
}
215277

216278
return result;
217279
}
280+
281+
private async *streamResponse(response: Response): AsyncGenerator<{text: string; metadata: any}> {
282+
const reader = response.body!.getReader();
283+
const decoder = new TextDecoder();
284+
285+
let buffer = "";
286+
let fullText = "";
287+
let finished = false;
288+
289+
while (!finished) {
290+
const {done, value} = await reader.read();
291+
if (done) break;
292+
293+
buffer += decoder.decode(value, {stream: true});
294+
let lines = buffer.split("\n");
295+
buffer = lines.pop() || "";
296+
297+
for (const line of lines) {
298+
if (!line.startsWith("data:")) continue;
299+
300+
const dataStr = line.slice("data:".length).trim();
301+
if (!dataStr || dataStr === "[DONE]") continue;
302+
303+
let json: any;
304+
try {
305+
json = JSON.parse(dataStr);
306+
} catch {
307+
continue;
308+
}
309+
310+
let deltaText = "";
311+
let metadata: any = undefined;
312+
313+
if (json.message) {
314+
const parts = json.message.content?.parts;
315+
metadata = json.message.metadata ?? json.metadata;
316+
317+
if (Array.isArray(parts) && typeof parts[0] === "string") {
318+
const current = parts[0];
319+
if (current.startsWith(fullText)) {
320+
deltaText = current.slice(fullText.length);
321+
} else {
322+
deltaText = current;
323+
}
324+
fullText = current;
325+
}
326+
327+
if (json.message.status === "finished_successfully") {
328+
finished = true;
329+
}
330+
}
331+
332+
if (json.o === "append" && json.p === "/message/content/parts/0") {
333+
deltaText += json.v;
334+
fullText += json.v;
335+
}
336+
337+
if (Array.isArray(json.v)) {
338+
for (const op of json.v) {
339+
if (op.o === "append" && op.p === "/message/content/parts/0") {
340+
deltaText += op.v;
341+
fullText += op.v;
342+
}
343+
if (op.p === "/message/status" && op.o === "replace" && op.v === "finished_successfully") {
344+
finished = true;
345+
}
346+
}
347+
}
348+
349+
if (json.type === "message_stream_complete") {
350+
finished = true;
351+
}
352+
353+
if (deltaText) {
354+
yield {
355+
text: deltaText,
356+
metadata: metadata ?? json.metadata ?? {},
357+
};
358+
}
359+
360+
if (finished) break;
361+
}
362+
}
363+
}
218364
}

src/utils/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export async function simulateBypassHeaders({
3232
Referer: "https://chatgpt.com/",
3333
"Referrer-Policy": "strict-origin-when-cross-origin",
3434
"oai-device-id": preOaiUUID || uuid,
35+
"oai-language": "en",
3536
"User-Agent": simulated.agent,
3637
pragma: "no-cache",
3738
priority: "u=1, i",
@@ -48,6 +49,9 @@ export async function simulateBypassHeaders({
4849
"X-Remote-Addr": ip,
4950
"X-Host": ip,
5051
"X-Forwarded-Host": ip,
52+
Forwarded: `for=${ip}`,
53+
"True-Client-IP": ip,
54+
"X-Real-IP": ip,
5155
}
5256
: {}),
5357
};

tests/answer.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,5 @@ test("Retrieves answer from oai", async () => {
66

77
const result = await chatgpt.complete("Hey, how are you?");
88
console.log("Result: ", result);
9-
10-
expect(result).toBeTypeOf("string");
9+
expect(typeof result).toBe("string");
1110
});

tests/stream.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {expect, test} from "vitest";
2+
import {ChatGPTReversed} from "../src/index";
3+
4+
test("Retrieves stream answer from oai", async () => {
5+
const chatgpt = new ChatGPTReversed();
6+
7+
const result = await chatgpt.complete("Hey, how are you?", {stream: true});
8+
9+
let streamData = "";
10+
for await (const chunk of result) {
11+
streamData += chunk.text;
12+
console.log("Chunk: ", chunk.text);
13+
}
14+
console.log("Streamed Result: ", streamData);
15+
16+
expect(streamData).toBeTypeOf("string");
17+
});

0 commit comments

Comments
 (0)