chatgpt-websocket 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/llmbackend.js +153 -0
  2. package/package.json +13 -0
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { WebSocketServer } = require('ws');
4
+ const crypto = require('crypto');
5
+
6
+ // Parse args: llmbackend token=<token> [port=<port>]
7
+ const args = {};
8
+ for (const arg of process.argv.slice(2)) {
9
+ const eq = arg.indexOf('=');
10
+ if (eq !== -1) {
11
+ args[arg.slice(0, eq)] = arg.slice(eq + 1);
12
+ }
13
+ }
14
+
15
+ const token = args.token;
16
+ if (!token) {
17
+ console.error('Usage: llmbackend token=<your_access_token> [port=8080]');
18
+ process.exit(1);
19
+ }
20
+
21
+ const PORT = parseInt(args.port, 10) || 8080;
22
+
23
+ // Decode JWT to extract chatgpt_account_id
24
+ let accountId;
25
+ try {
26
+ const payload = JSON.parse(
27
+ Buffer.from(token.split('.')[1], 'base64url').toString()
28
+ );
29
+ accountId = payload['https://api.openai.com/auth']?.chatgpt_account_id;
30
+ } catch (e) {
31
+ // ignore
32
+ }
33
+ if (!accountId) {
34
+ console.error('Could not extract chatgpt_account_id from token');
35
+ process.exit(1);
36
+ }
37
+
38
+ const INSTRUCTIONS = `You are a coding assistant. You help the user with software engineering tasks including writing code, debugging, and explaining code. Be concise and direct.`;
39
+
40
+ async function streamChat(message, history, ws) {
41
+ // Add user message to history
42
+ history.push({
43
+ type: 'message',
44
+ role: 'user',
45
+ content: [{ type: 'input_text', text: message }]
46
+ });
47
+
48
+ const body = JSON.stringify({
49
+ model: 'gpt-5',
50
+ instructions: INSTRUCTIONS,
51
+ input: history,
52
+ store: false,
53
+ stream: true,
54
+ include: ['reasoning.encrypted_content'],
55
+ tools: [],
56
+ tool_choice: 'none'
57
+ });
58
+
59
+ const res = await fetch('https://chatgpt.com/backend-api/codex/responses', {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Content-Type': 'application/json',
63
+ 'Authorization': `Bearer ${token}`,
64
+ 'Accept': 'text/event-stream',
65
+ 'OpenAI-Beta': 'responses=experimental',
66
+ 'session_id': crypto.randomUUID(),
67
+ 'originator': 'codex_cli_rs',
68
+ 'chatgpt-account-id': accountId
69
+ },
70
+ body
71
+ });
72
+
73
+ if (!res.ok) {
74
+ const errText = await res.text();
75
+ ws.send(JSON.stringify({ type: 'error', error: `HTTP ${res.status}: ${errText}` }));
76
+ return;
77
+ }
78
+
79
+ const reader = res.body.getReader();
80
+ const decoder = new TextDecoder();
81
+ let buffer = '';
82
+ let assistantText = '';
83
+
84
+ while (true) {
85
+ const { done, value } = await reader.read();
86
+ if (done) break;
87
+
88
+ buffer += decoder.decode(value, { stream: true });
89
+ const lines = buffer.split('\n');
90
+ buffer = lines.pop();
91
+
92
+ for (const line of lines) {
93
+ if (!line.startsWith('data: ')) continue;
94
+ const data = line.slice(6).trim();
95
+ if (data === '[DONE]') {
96
+ history.push({ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: assistantText }] });
97
+ ws.send(JSON.stringify({ type: 'done' }));
98
+ return;
99
+ }
100
+
101
+ try {
102
+ const event = JSON.parse(data);
103
+ if (event.type === 'response.output_text.delta' && event.delta) {
104
+ assistantText += event.delta;
105
+ ws.send(JSON.stringify({ type: 'chunk', content: event.delta }));
106
+ }
107
+ if (event.type === 'response.completed') {
108
+ history.push({ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: assistantText }] });
109
+ ws.send(JSON.stringify({ type: 'done' }));
110
+ return;
111
+ }
112
+ } catch (e) {
113
+ // skip non-JSON lines
114
+ }
115
+ }
116
+ }
117
+
118
+ history.push({ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: assistantText }] });
119
+ ws.send(JSON.stringify({ type: 'done' }));
120
+ }
121
+
122
+ const wss = new WebSocketServer({ port: PORT });
123
+
124
+ wss.on('connection', (ws) => {
125
+ console.log('Client connected');
126
+ const history = [];
127
+
128
+ ws.on('message', async (raw) => {
129
+ let msg;
130
+ try {
131
+ msg = JSON.parse(raw.toString());
132
+ } catch (e) {
133
+ ws.send(JSON.stringify({ type: 'error', error: 'Invalid JSON' }));
134
+ return;
135
+ }
136
+
137
+ if (msg.type !== 'chat' || !msg.message) {
138
+ ws.send(JSON.stringify({ type: 'error', error: 'Expected {"type":"chat","message":"..."}' }));
139
+ return;
140
+ }
141
+
142
+ try {
143
+ await streamChat(msg.message, history, ws);
144
+ } catch (err) {
145
+ console.error('Stream error:', err);
146
+ ws.send(JSON.stringify({ type: 'error', error: err.message }));
147
+ }
148
+ });
149
+
150
+ ws.on('close', () => console.log('Client disconnected'));
151
+ });
152
+
153
+ console.log(`llmbackend running on ws://localhost:${PORT}`);
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "chatgpt-websocket",
3
+ "version": "1.0.0",
4
+ "description": "WebSocket server wrapping ChatGPT backend API for GPT-5 access",
5
+ "bin": {
6
+ "chatgpt-websocket": "bin/llmbackend.js"
7
+ },
8
+ "keywords": ["chatgpt", "gpt-5", "websocket", "openai", "streaming"],
9
+ "license": "MIT",
10
+ "dependencies": {
11
+ "ws": "^8.18.0"
12
+ }
13
+ }