@miruzero/zgpt 0.1.0 → 0.1.1
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.
- package/docs/architecture.md +29 -3
- package/package.json +1 -1
- package/src/auth/index.js +111 -84
- package/src/cli/conversations-tui.js +301 -0
- package/src/cli/formatter.js +39 -1
- package/src/cli.js +177 -113
- package/src/config/paths.js +3 -1
- package/src/connector/read.js +28 -4
- package/src/connector/write.js +7 -7
- package/src/conversation/context-config.js +68 -0
- package/src/conversation/index.js +11 -5
- package/src/conversation/read.js +10 -3
- package/src/conversation/recovery-state.js +51 -0
- package/src/conversation/recovery.js +85 -0
- package/src/conversation/temporary.js +3 -1
- package/src/conversation/write.js +118 -27
- package/src/file/index.js +151 -0
- package/src/index.js +5 -1
- package/src/memory/index.js +64 -0
- package/src/protocol/sse.js +2 -1
- package/src/runtime-context.js +27 -0
- package/src/session/direct-transport.js +68 -0
- package/src/session/exchange.js +9 -4
- package/src/session/index.js +100 -68
- package/src/session/local-storage.js +99 -68
- package/src/settings/index.js +18 -0
- package/src/share/index.js +25 -0
- package/src/transport/direct/http-client.js +189 -164
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const read = require('./read');
|
|
4
|
+
const stateStore = require('./recovery-state');
|
|
5
|
+
const sse = require('../protocol/sse');
|
|
6
|
+
|
|
7
|
+
const coded = (code, message, extra = {}) => Object.assign(new Error(message), { code, ...extra });
|
|
8
|
+
|
|
9
|
+
const verifyCanonical = async ({ checkpoint, env, token, sessionHeaders, requestJson }) => {
|
|
10
|
+
const conversation = await read.getConversation({
|
|
11
|
+
conversationId: checkpoint.conversation_id, token, sessionHeaders, requestJson
|
|
12
|
+
});
|
|
13
|
+
const currentNode = conversation?.current_node || null;
|
|
14
|
+
const parentNode = checkpoint.parent_message_id || null;
|
|
15
|
+
if (!currentNode || (parentNode && currentNode === parentNode)) {
|
|
16
|
+
throw coded('STREAM_CANONICAL_UNVERIFIED', 'conversation canonical state has not advanced');
|
|
17
|
+
}
|
|
18
|
+
stateStore.remove(checkpoint.conversation_id, env);
|
|
19
|
+
return conversation;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
exports.retry = async ({
|
|
23
|
+
conversationId, env = process.env, token, sessionHeaders,
|
|
24
|
+
requestJson, requestText, onChunk = null
|
|
25
|
+
}) => {
|
|
26
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
27
|
+
const checkpoint = stateStore.get(conversationId, env);
|
|
28
|
+
if (!checkpoint) throw coded('STREAM_RECOVERY_STATE_MISSING', 'conversation recovery checkpoint is missing');
|
|
29
|
+
|
|
30
|
+
if (checkpoint.phase === 'verify') {
|
|
31
|
+
return verifyCanonical({ checkpoint, env, token, sessionHeaders, requestJson });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const streamState = await read.getConversationStreamStatus({
|
|
35
|
+
conversationId, turnTraceId: checkpoint.turn_trace_id, token, sessionHeaders, requestJson
|
|
36
|
+
});
|
|
37
|
+
const status = streamState?.status || null;
|
|
38
|
+
|
|
39
|
+
if (status === 'FAILURE') throw coded('STREAM_FAILED', 'conversation stream reported failure');
|
|
40
|
+
if (status === 'COMPLETE' || status === 'IS_STOP_REQUESTED' || status === 'UNAVAILABLE') {
|
|
41
|
+
stateStore.save({
|
|
42
|
+
conversationId, phase: 'verify', offset: checkpoint.offset,
|
|
43
|
+
resumeToken: checkpoint.resume_token, turnTraceId: checkpoint.turn_trace_id,
|
|
44
|
+
parentMessageId: checkpoint.parent_message_id, currentNode: checkpoint.current_node
|
|
45
|
+
}, env);
|
|
46
|
+
return verifyCanonical({ checkpoint: stateStore.get(conversationId, env), env, token, sessionHeaders, requestJson });
|
|
47
|
+
}
|
|
48
|
+
if (status !== 'IS_STREAMING') {
|
|
49
|
+
throw coded('STREAM_STATUS_UNKNOWN', `unknown conversation stream status: ${status || 'missing'}`);
|
|
50
|
+
}
|
|
51
|
+
if (!checkpoint.resume_token) {
|
|
52
|
+
throw coded('STREAM_RESUME_TOKEN_REQUIRED', 'resume token required for frozen streaming conversation');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const parser = sse.createParser();
|
|
56
|
+
let sawChunk = false;
|
|
57
|
+
let response;
|
|
58
|
+
try {
|
|
59
|
+
response = await read.resumeConversation({
|
|
60
|
+
conversationId,
|
|
61
|
+
offset: checkpoint.offset,
|
|
62
|
+
conduitToken: checkpoint.resume_token,
|
|
63
|
+
turnTraceId: checkpoint.turn_trace_id,
|
|
64
|
+
token, sessionHeaders, requestText,
|
|
65
|
+
onChunk: (chunk) => { sawChunk = true; parser.push(chunk); onChunk?.(chunk); }
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (!error.code) error.code = 'STREAM_RESUME_FAILED';
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
if (response?.text && !sawChunk) parser.push(response.text);
|
|
72
|
+
const resumed = parser.finish();
|
|
73
|
+
const nextOffset = checkpoint.offset + (resumed.resumeOffset || 0);
|
|
74
|
+
stateStore.save({
|
|
75
|
+
conversationId,
|
|
76
|
+
phase: resumed.completed ? 'verify' : 'stream',
|
|
77
|
+
offset: nextOffset,
|
|
78
|
+
resumeToken: resumed.resumeToken || checkpoint.resume_token,
|
|
79
|
+
turnTraceId: checkpoint.turn_trace_id,
|
|
80
|
+
parentMessageId: checkpoint.parent_message_id,
|
|
81
|
+
currentNode: resumed.currentNode || checkpoint.current_node
|
|
82
|
+
}, env);
|
|
83
|
+
if (!resumed.completed) throw coded('STREAM_INCOMPLETE', 'resumed conversation stream ended before [DONE]');
|
|
84
|
+
return verifyCanonical({ checkpoint: stateStore.get(conversationId, env), env, token, sessionHeaders, requestJson });
|
|
85
|
+
};
|
|
@@ -23,6 +23,7 @@ exports.createConversation = async ({
|
|
|
23
23
|
}) => {
|
|
24
24
|
const result = await write.createConversation({
|
|
25
25
|
...options,
|
|
26
|
+
env,
|
|
26
27
|
message,
|
|
27
28
|
temporaryChat: true,
|
|
28
29
|
historyAndTrainingDisabled: true,
|
|
@@ -46,6 +47,7 @@ exports.sendConversation = async ({
|
|
|
46
47
|
}
|
|
47
48
|
const result = await write.sendConversation({
|
|
48
49
|
...options,
|
|
50
|
+
env,
|
|
49
51
|
conversationId,
|
|
50
52
|
message,
|
|
51
53
|
parentMessageId: cursor.current_node,
|
|
@@ -56,4 +58,4 @@ exports.sendConversation = async ({
|
|
|
56
58
|
});
|
|
57
59
|
tempState.save({ conversationId, currentNode: result.current_node, env });
|
|
58
60
|
return result;
|
|
59
|
-
};
|
|
61
|
+
};
|
|
@@ -6,8 +6,10 @@ const sentinel = require('../protocol/sentinel-client');
|
|
|
6
6
|
const prepare = require('../protocol/conversation-payload');
|
|
7
7
|
const conduit = require('../protocol/conduit');
|
|
8
8
|
const sse = require('../protocol/sse');
|
|
9
|
-
const read = require('./read');
|
|
10
|
-
const
|
|
9
|
+
const read = require('./read');
|
|
10
|
+
const recoveryState = require('./recovery-state');
|
|
11
|
+
const connector = require('../connector');
|
|
12
|
+
const contextConfig = require('./context-config');
|
|
11
13
|
|
|
12
14
|
const requestConversationInit = async ({
|
|
13
15
|
conversationId = null,
|
|
@@ -129,7 +131,8 @@ exports.generateConversationTitle = async ({ conversationId, messageId, token, s
|
|
|
129
131
|
};
|
|
130
132
|
|
|
131
133
|
const sendWithRecovery = async ({
|
|
132
|
-
target, options, conversationId = null,
|
|
134
|
+
target, options, conversationId = null, parentMessageId = null, turnTraceId,
|
|
135
|
+
env = process.env, persistRecovery = true,
|
|
133
136
|
token, sessionHeaders, requestJson, requestText
|
|
134
137
|
}) => {
|
|
135
138
|
const progress = sse.createParser();
|
|
@@ -146,25 +149,96 @@ const sendWithRecovery = async ({
|
|
|
146
149
|
if (response?.text && !sawChunk) progress.push(response.text);
|
|
147
150
|
const state = progress.finish();
|
|
148
151
|
const observedId = state.conversationId || conversationId;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
const checkpoint = (phase, cursor = {}) => ({
|
|
153
|
+
conversationId: observedId,
|
|
154
|
+
phase,
|
|
155
|
+
offset: cursor.offset ?? state.resumeOffset ?? 0,
|
|
156
|
+
resumeToken: cursor.resumeToken ?? state.resumeToken ?? null,
|
|
157
|
+
turnTraceId,
|
|
158
|
+
parentMessageId,
|
|
159
|
+
currentNode: cursor.currentNode ?? state.currentNode ?? null
|
|
153
160
|
});
|
|
161
|
+
const freeze = (error, value) => {
|
|
162
|
+
if (persistRecovery && observedId) recoveryState.save(value, env);
|
|
163
|
+
throw error;
|
|
164
|
+
};
|
|
165
|
+
if (state.completed) {
|
|
166
|
+
return {
|
|
167
|
+
response: response || { status: 200, text: '' }, conversationId: observedId,
|
|
168
|
+
currentNode: state.currentNode, recoveryCheckpoint: observedId ? checkpoint('verify') : null
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (!observedId) {
|
|
172
|
+
if (upstreamError) throw upstreamError;
|
|
173
|
+
throw Object.assign(new Error('conversation stream ended before conversation_id was available'), { code: 'STREAM_INCOMPLETE' });
|
|
174
|
+
}
|
|
175
|
+
const interruptedCheckpoint = checkpoint('stream');
|
|
176
|
+
let streamState;
|
|
177
|
+
try {
|
|
178
|
+
streamState = await read.getConversationStreamStatus({
|
|
179
|
+
conversationId: observedId, turnTraceId, token, sessionHeaders, requestJson
|
|
180
|
+
});
|
|
181
|
+
} catch (error) {
|
|
182
|
+
freeze(error, interruptedCheckpoint);
|
|
183
|
+
}
|
|
184
|
+
if (streamState?.status === 'FAILURE') {
|
|
185
|
+
freeze(Object.assign(new Error('conversation stream reported failure'), { code: 'STREAM_FAILED' }), checkpoint('stream'));
|
|
186
|
+
}
|
|
187
|
+
if (['COMPLETE', 'IS_STOP_REQUESTED', 'UNAVAILABLE'].includes(streamState?.status)) {
|
|
188
|
+
return {
|
|
189
|
+
response: response || { status: 200, text: '' }, conversationId: observedId,
|
|
190
|
+
currentNode: state.currentNode, recoveryCheckpoint: checkpoint('verify')
|
|
191
|
+
};
|
|
192
|
+
}
|
|
154
193
|
if (streamState?.status !== 'IS_STREAMING') {
|
|
155
|
-
|
|
194
|
+
freeze(Object.assign(new Error(`unknown conversation stream status: ${streamState?.status || 'missing'}`), { code: 'STREAM_STATUS_UNKNOWN' }), checkpoint('stream'));
|
|
195
|
+
}
|
|
196
|
+
const resumeOffset = state.resumeOffset || 0;
|
|
197
|
+
const streamCheckpoint = checkpoint('stream', { offset: resumeOffset, resumeToken: state.resumeToken });
|
|
198
|
+
if (!state.resumeToken) {
|
|
199
|
+
freeze(Object.assign(new Error('resume token required for interrupted streaming conversation'), { code: 'STREAM_RESUME_TOKEN_REQUIRED' }), streamCheckpoint);
|
|
156
200
|
}
|
|
157
201
|
const resumedProgress = sse.createParser();
|
|
158
202
|
let resumedSawChunk = false;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
203
|
+
let resumed;
|
|
204
|
+
try {
|
|
205
|
+
resumed = await read.resumeConversation({
|
|
206
|
+
conversationId: observedId, offset: resumeOffset, conduitToken: state.resumeToken, turnTraceId,
|
|
207
|
+
token, sessionHeaders, requestText,
|
|
208
|
+
onChunk: (chunk) => { resumedSawChunk = true; resumedProgress.push(chunk); options.onChunk?.(chunk); }
|
|
209
|
+
});
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (!error.code) error.code = 'STREAM_RESUME_FAILED';
|
|
212
|
+
freeze(error, streamCheckpoint);
|
|
213
|
+
}
|
|
164
214
|
if (resumed.text && !resumedSawChunk) resumedProgress.push(resumed.text);
|
|
165
215
|
const resumedState = resumedProgress.finish();
|
|
166
|
-
|
|
167
|
-
|
|
216
|
+
const nextCheckpoint = checkpoint(resumedState.completed ? 'verify' : 'stream', {
|
|
217
|
+
offset: resumeOffset + (resumedState.resumeOffset || 0),
|
|
218
|
+
resumeToken: resumedState.resumeToken || state.resumeToken,
|
|
219
|
+
currentNode: resumedState.currentNode || state.currentNode
|
|
220
|
+
});
|
|
221
|
+
if (!resumedState.completed) {
|
|
222
|
+
freeze(Object.assign(new Error('resumed conversation stream ended before [DONE]'), { code: 'STREAM_INCOMPLETE' }), nextCheckpoint);
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
response: response || { status: 200, text: '' }, conversationId: observedId,
|
|
226
|
+
currentNode: resumedState.currentNode || state.currentNode, recoveryCheckpoint: nextCheckpoint
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const canonicalAfterDispatch = async ({ conversationId, parentMessageId = null, checkpoint = null, env = process.env, token, sessionHeaders, requestJson }) => {
|
|
231
|
+
try {
|
|
232
|
+
const conversation = await read.getConversation({ conversationId, token, sessionHeaders, requestJson });
|
|
233
|
+
if (!conversation?.current_node || (parentMessageId && conversation.current_node === parentMessageId)) {
|
|
234
|
+
throw Object.assign(new Error('conversation canonical state has not advanced'), { code: 'STREAM_CANONICAL_UNVERIFIED' });
|
|
235
|
+
}
|
|
236
|
+
recoveryState.remove(conversationId, env);
|
|
237
|
+
return conversation;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (checkpoint) recoveryState.save(checkpoint, env);
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
168
242
|
};
|
|
169
243
|
exports.createConversation = async ({
|
|
170
244
|
message,
|
|
@@ -191,17 +265,22 @@ exports.createConversation = async ({
|
|
|
191
265
|
forceParallelSwitch = 'auto',
|
|
192
266
|
paragenCotSummaryDisplayOverride = 'allow',
|
|
193
267
|
onChunk = null,
|
|
194
|
-
recoverStream =
|
|
268
|
+
recoverStream = null,
|
|
269
|
+
env = process.env,
|
|
195
270
|
token,
|
|
196
271
|
sessionHeaders,
|
|
197
272
|
resolveSystemHintSelections = connector.resolveSystemHintSelections,
|
|
198
273
|
requestJson = direct.requestJson,
|
|
199
274
|
requestText = direct.requestText
|
|
200
275
|
}) => {
|
|
276
|
+
const shouldRecoverStream = recoverStream == null ? !temporaryChat : recoverStream === true;
|
|
201
277
|
if (!message) throw new Error('message is required');
|
|
202
278
|
if (!Array.isArray(systemHints) && !Array.isArray(systemHintMentions)) ({ systemHints, systemHintMentions } = await resolveSystemHintSelections({ message, token, sessionHeaders }));
|
|
203
279
|
systemHints = Array.isArray(systemHints) ? systemHints : [];
|
|
204
|
-
systemHintMentions = Array.isArray(systemHintMentions) ? systemHintMentions : [];
|
|
280
|
+
systemHintMentions = Array.isArray(systemHintMentions) ? systemHintMentions : [];
|
|
281
|
+
({ systemHints, localFunctionNames, hiddenSystemMessages } = contextConfig.merge({
|
|
282
|
+
env, systemHints, localFunctionNames, hiddenSystemMessages
|
|
283
|
+
}));
|
|
205
284
|
|
|
206
285
|
let preInit = null;
|
|
207
286
|
let preInitError = null;
|
|
@@ -242,7 +321,7 @@ exports.createConversation = async ({
|
|
|
242
321
|
if (fresh?.token) sentinelHeaders = { ...sentinelHeaders, 'openai-sentinel-chat-requirements-token': fresh.token };
|
|
243
322
|
if (fresh?.proofToken) sentinelHeaders = { ...sentinelHeaders, 'openai-sentinel-proof-token': fresh.proofToken };
|
|
244
323
|
} catch {
|
|
245
|
-
// fallback:
|
|
324
|
+
// fallback: reuse sentinel headers from the current session when refresh fails
|
|
246
325
|
}
|
|
247
326
|
const sendTarget = '/backend-api/f/conversation';
|
|
248
327
|
const sendOptions = {
|
|
@@ -257,9 +336,9 @@ exports.createConversation = async ({
|
|
|
257
336
|
},
|
|
258
337
|
onChunk
|
|
259
338
|
};
|
|
260
|
-
const sent =
|
|
339
|
+
const sent = shouldRecoverStream
|
|
261
340
|
? await sendWithRecovery({
|
|
262
|
-
target: sendTarget, options: sendOptions,
|
|
341
|
+
target: sendTarget, options: sendOptions, turnTraceId: traceId, env, persistRecovery: !temporaryChat,
|
|
263
342
|
token, sessionHeaders, requestJson, requestText
|
|
264
343
|
})
|
|
265
344
|
: { response: await requestText(sendTarget, token, sessionHeaders, sendOptions) };
|
|
@@ -275,7 +354,9 @@ exports.createConversation = async ({
|
|
|
275
354
|
if (!currentNode) throw Object.assign(new Error('temporary conversation response missing current node'), { code: 'TEMP_CURRENT_NODE_MISSING' });
|
|
276
355
|
return { conversation_id: conversationId, current_node: currentNode, temporary_chat: true, messages: [] };
|
|
277
356
|
}
|
|
278
|
-
const conversation =
|
|
357
|
+
const conversation = shouldRecoverStream
|
|
358
|
+
? await canonicalAfterDispatch({ conversationId, checkpoint: sent.recoveryCheckpoint, env, token, sessionHeaders, requestJson })
|
|
359
|
+
: await read.getConversation({ conversationId, token, sessionHeaders, requestJson });
|
|
279
360
|
if (preInit) {
|
|
280
361
|
conversation.init = preInit;
|
|
281
362
|
} else {
|
|
@@ -316,18 +397,26 @@ const sendConversationDirect = async ({
|
|
|
316
397
|
forceParallelSwitch = 'auto',
|
|
317
398
|
paragenCotSummaryDisplayOverride = 'allow',
|
|
318
399
|
onChunk = null,
|
|
319
|
-
recoverStream =
|
|
400
|
+
recoverStream = null,
|
|
401
|
+
env = process.env,
|
|
320
402
|
token,
|
|
321
403
|
sessionHeaders,
|
|
322
404
|
resolveSystemHintSelections = connector.resolveSystemHintSelections,
|
|
323
405
|
requestJson = direct.requestJson,
|
|
324
406
|
requestText = direct.requestText
|
|
325
407
|
}) => {
|
|
408
|
+
const shouldRecoverStream = recoverStream == null ? !temporaryChat : recoverStream === true;
|
|
326
409
|
if (!conversationId) throw new Error('conversationId is required');
|
|
410
|
+
if (!temporaryChat && recoveryState.get(conversationId, env)) {
|
|
411
|
+
throw Object.assign(new Error('conversation has a frozen recovery checkpoint; retry it before sending a new message'), { code: 'STREAM_RECOVERY_PENDING' });
|
|
412
|
+
}
|
|
327
413
|
if (!message) throw new Error('message is required');
|
|
328
414
|
if (!Array.isArray(systemHints) && !Array.isArray(systemHintMentions)) ({ systemHints, systemHintMentions } = await resolveSystemHintSelections({ message, token, sessionHeaders }));
|
|
329
415
|
systemHints = Array.isArray(systemHints) ? systemHints : [];
|
|
330
|
-
systemHintMentions = Array.isArray(systemHintMentions) ? systemHintMentions : [];
|
|
416
|
+
systemHintMentions = Array.isArray(systemHintMentions) ? systemHintMentions : [];
|
|
417
|
+
({ systemHints, localFunctionNames, hiddenSystemMessages } = contextConfig.merge({
|
|
418
|
+
env, systemHints, localFunctionNames, hiddenSystemMessages
|
|
419
|
+
}));
|
|
331
420
|
let resolvedParentMessageId = parentMessageId;
|
|
332
421
|
let resolvedProjectId = projectId;
|
|
333
422
|
if (!temporaryChat) {
|
|
@@ -342,7 +431,7 @@ const sendConversationDirect = async ({
|
|
|
342
431
|
if (fresh?.token) sentinelHeaders = { ...sentinelHeaders, 'openai-sentinel-chat-requirements-token': fresh.token };
|
|
343
432
|
if (fresh?.proofToken) sentinelHeaders = { ...sentinelHeaders, 'openai-sentinel-proof-token': fresh.proofToken };
|
|
344
433
|
} catch {
|
|
345
|
-
// fallback:
|
|
434
|
+
// fallback: reuse sentinel headers from the current session when refresh fails
|
|
346
435
|
}
|
|
347
436
|
const turnHeaders = { ...sessionHeaders };
|
|
348
437
|
for (const name of conduit.SENTINEL_HEADER_NAMES) delete turnHeaders[name];
|
|
@@ -397,9 +486,9 @@ const sendConversationDirect = async ({
|
|
|
397
486
|
},
|
|
398
487
|
onChunk
|
|
399
488
|
};
|
|
400
|
-
const sent =
|
|
489
|
+
const sent = shouldRecoverStream
|
|
401
490
|
? await sendWithRecovery({
|
|
402
|
-
target: sendTarget, options: sendOptions, conversationId,
|
|
491
|
+
target: sendTarget, options: sendOptions, conversationId, parentMessageId: resolvedParentMessageId, turnTraceId: traceId, env, persistRecovery: !temporaryChat,
|
|
403
492
|
token, sessionHeaders: turnHeaders, requestJson, requestText
|
|
404
493
|
})
|
|
405
494
|
: { response: await requestText(sendTarget, token, turnHeaders, sendOptions) };
|
|
@@ -417,7 +506,9 @@ const sendConversationDirect = async ({
|
|
|
417
506
|
messages: []
|
|
418
507
|
};
|
|
419
508
|
}
|
|
420
|
-
const after =
|
|
509
|
+
const after = shouldRecoverStream
|
|
510
|
+
? await canonicalAfterDispatch({ conversationId, parentMessageId: resolvedParentMessageId, checkpoint: sent.recoveryCheckpoint, env, token, sessionHeaders, requestJson })
|
|
511
|
+
: await read.getConversation({ conversationId, token, sessionHeaders, requestJson });
|
|
421
512
|
if (after && typeof after === 'object') after.previous_node = resolvedParentMessageId;
|
|
422
513
|
return after;
|
|
423
514
|
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const https = require('node:https');
|
|
3
|
+
const direct = require('../transport/direct/http-client');
|
|
4
|
+
const context = require('../runtime-context');
|
|
5
|
+
|
|
6
|
+
const required = (value, name) => {
|
|
7
|
+
if (typeof value !== 'string' || !value) throw Object.assign(new Error(`${name} is required`), { code: 'BAD_ARGS' });
|
|
8
|
+
return value;
|
|
9
|
+
};
|
|
10
|
+
const require2xx = (response, target) => {
|
|
11
|
+
if (!response || response.status < 200 || response.status >= 300) {
|
|
12
|
+
const error = new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`);
|
|
13
|
+
error.status = response?.status || null;
|
|
14
|
+
error.target = target;
|
|
15
|
+
if (error.status === 429) error.code = 'RATE_LIMITED';
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
return response;
|
|
19
|
+
};
|
|
20
|
+
const normalizeReservation = (input) => {
|
|
21
|
+
if (!input || input.eligible !== true) throw Object.assign(new Error('upload reservation is not eligible'), { code: 'UPLOAD_RESERVATION_UNAVAILABLE' });
|
|
22
|
+
return {
|
|
23
|
+
fileId: required(input.reservation_id, 'reservation_id'),
|
|
24
|
+
uploadUrl: required(input.upload_url, 'upload_url'),
|
|
25
|
+
uploadUrlExpiresAt: required(input.upload_url_expires_at, 'upload_url_expires_at'),
|
|
26
|
+
reservationExpiresAt: required(input.reservation_expires_at, 'reservation_expires_at')
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const uploadDefaults = (options) => ({
|
|
30
|
+
entrySurface: options.entrySurface || 'chat_composer',
|
|
31
|
+
storeInLibrary: options.storeInLibrary === true,
|
|
32
|
+
libraryPersistenceMode: options.libraryPersistenceMode || 'opportunistic'
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
exports.createUploadReservation = async (options = {}) => {
|
|
36
|
+
options = await context.bind(options);
|
|
37
|
+
const intendedUseCase = required(options.intendedUseCase, 'intendedUseCase');
|
|
38
|
+
const d = uploadDefaults(options);
|
|
39
|
+
const target = '/backend-api/files/upload_reservations';
|
|
40
|
+
const response = require2xx(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, {
|
|
41
|
+
method: 'POST', retry: false, route: target,
|
|
42
|
+
body: { intended_use_case: intendedUseCase, entry_surface: d.entrySurface, requires_gizmo_id: false, store_in_library: d.storeInLibrary, library_persistence_mode: d.libraryPersistenceMode }
|
|
43
|
+
}), target);
|
|
44
|
+
return normalizeReservation(response.json);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
exports.createFileEntry = async (options = {}) => {
|
|
48
|
+
options = await context.bind(options);
|
|
49
|
+
const name = required(options.name, 'name');
|
|
50
|
+
const useCase = required(options.useCase, 'useCase');
|
|
51
|
+
if (!Number.isFinite(options.size) || options.size < 0) throw Object.assign(new Error('size is required'), { code: 'BAD_ARGS' });
|
|
52
|
+
const d = uploadDefaults(options);
|
|
53
|
+
const target = '/backend-api/files';
|
|
54
|
+
const body = {
|
|
55
|
+
file_name: name, file_size: options.size, use_case: useCase,
|
|
56
|
+
timezone_offset_min: Number.isFinite(options.timezoneOffsetMin) ? options.timezoneOffsetMin : new Date().getTimezoneOffset(),
|
|
57
|
+
reset_rate_limits: options.resetRateLimits === true, supports_direct_azure_multipart: true,
|
|
58
|
+
entry_surface: d.entrySurface, store_in_library: d.storeInLibrary, library_persistence_mode: d.libraryPersistenceMode
|
|
59
|
+
};
|
|
60
|
+
if (options.mimeType) body.mime_type = options.mimeType;
|
|
61
|
+
const response = require2xx(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { method: 'POST', retry: false, route: target, body }), target);
|
|
62
|
+
return response.json || {};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
exports.claimUploadReservation = async (options = {}) => {
|
|
66
|
+
options = await context.bind(options);
|
|
67
|
+
const reservationId = required(options.reservationId, 'reservationId');
|
|
68
|
+
const name = required(options.name, 'name');
|
|
69
|
+
const useCase = required(options.useCase, 'useCase');
|
|
70
|
+
if (!Number.isFinite(options.size) || options.size < 0) throw Object.assign(new Error('size is required'), { code: 'BAD_ARGS' });
|
|
71
|
+
const d = uploadDefaults(options);
|
|
72
|
+
const target = `/backend-api/files/upload_reservations/${encodeURIComponent(reservationId)}/claim_and_finish`;
|
|
73
|
+
const body = {
|
|
74
|
+
file_name: name, file_size: options.size, use_case: useCase,
|
|
75
|
+
index_for_retrieval: options.indexForRetrieval === true,
|
|
76
|
+
store_in_library: d.storeInLibrary, library_persistence_mode: d.libraryPersistenceMode,
|
|
77
|
+
entry_surface: d.entrySurface
|
|
78
|
+
};
|
|
79
|
+
if (options.mimeType) body.mime_type = options.mimeType;
|
|
80
|
+
if (options.metadata) body.metadata = options.metadata;
|
|
81
|
+
const headers = options.modelSlug ? { 'x-oai-model-slug': options.modelSlug } : undefined;
|
|
82
|
+
const response = require2xx(await (options.requestText || direct.requestText)(target, options.token, options.sessionHeaders, { method: 'POST', retry: false, route: '/backend-api/files/upload_reservations/{reservation_id}/claim_and_finish', body, ...(headers ? { headers } : {}) }), target);
|
|
83
|
+
return response.text ?? '';
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
exports.processUpload = async (options = {}) => {
|
|
87
|
+
options = await context.bind(options);
|
|
88
|
+
const fileId = required(options.fileId, 'fileId');
|
|
89
|
+
const name = required(options.name, 'name');
|
|
90
|
+
const mimeType = required(options.mimeType, 'mimeType');
|
|
91
|
+
const d = uploadDefaults(options);
|
|
92
|
+
const useCase = options.useCase || (mimeType.startsWith('image/') ? 'multimodal' : 'my_files');
|
|
93
|
+
const target = '/backend-api/files/process_upload_stream';
|
|
94
|
+
const body = {
|
|
95
|
+
entry_surface: d.entrySurface, file_id: fileId, file_name: name,
|
|
96
|
+
index_for_retrieval: options.indexForRetrieval ?? !mimeType.startsWith('image/'),
|
|
97
|
+
library_persistence_mode: d.libraryPersistenceMode,
|
|
98
|
+
metadata: { is_temporary_chat: options.temporary === true, store_in_library: d.storeInLibrary, is_project_thread: options.isProjectThread === true },
|
|
99
|
+
mime_type: mimeType, use_case: useCase
|
|
100
|
+
};
|
|
101
|
+
const response = require2xx(await (options.requestText || direct.requestText)(target, options.token, options.sessionHeaders, { method: 'POST', retry: false, route: target, body }), target);
|
|
102
|
+
return response.text ?? '';
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
const signedTargetForLog = (value) => {
|
|
109
|
+
try {
|
|
110
|
+
const url = new URL(value);
|
|
111
|
+
return url.origin + url.pathname + (url.search ? '?<redacted>' : '');
|
|
112
|
+
} catch { return '<invalid-upload-url>'; }
|
|
113
|
+
};
|
|
114
|
+
const hasHeader = (headers, name) => Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
|
|
115
|
+
const putSignedBytes = (url, data, headers) => new Promise((resolve, reject) => {
|
|
116
|
+
const req = https.request(url, { method: 'PUT', headers }, (res) => {
|
|
117
|
+
res.resume();
|
|
118
|
+
res.on('end', () => resolve({ status: Number(res.statusCode || 0), headers: { ...res.headers } }));
|
|
119
|
+
res.on('error', reject);
|
|
120
|
+
});
|
|
121
|
+
req.on('error', reject);
|
|
122
|
+
req.end(data);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
exports.uploadBytes = async (options = {}) => {
|
|
126
|
+
const uploadUrl = required(options.uploadUrl, 'uploadUrl');
|
|
127
|
+
let parsed;
|
|
128
|
+
try { parsed = new URL(uploadUrl); } catch { throw Object.assign(new Error('uploadUrl is invalid'), { code: 'BAD_ARGS' }); }
|
|
129
|
+
if (parsed.protocol !== 'https:') throw Object.assign(new Error('uploadUrl must use https'), { code: 'BAD_ARGS' });
|
|
130
|
+
const data = Buffer.isBuffer(options.data) ? options.data : Buffer.from(options.data ?? '');
|
|
131
|
+
const headers = { ...(options.uploadHeaders || {}) };
|
|
132
|
+
if (options.mimeType && !hasHeader(headers, 'content-type')) headers['content-type'] = options.mimeType;
|
|
133
|
+
if (!hasHeader(headers, 'content-length')) headers['content-length'] = String(data.length);
|
|
134
|
+
let response;
|
|
135
|
+
try { response = await (options.putBytes || putSignedBytes)(uploadUrl, data, headers); }
|
|
136
|
+
catch (cause) {
|
|
137
|
+
const error = new Error('ChatGPT signed file upload transport failed');
|
|
138
|
+
error.code = 'FILE_BLOB_UPLOAD_FAILED';
|
|
139
|
+
error.target = signedTargetForLog(uploadUrl);
|
|
140
|
+
error.cause = cause;
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
if (!response || response.status < 200 || response.status >= 300) {
|
|
144
|
+
const error = new Error(`ChatGPT signed file upload failed (${response?.status || 'no-status'})`);
|
|
145
|
+
error.code = 'FILE_BLOB_UPLOAD_FAILED';
|
|
146
|
+
error.status = response?.status || null;
|
|
147
|
+
error.target = signedTargetForLog(uploadUrl);
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
return { status: response.status, headers: response.headers || {} };
|
|
151
|
+
};
|
package/src/index.js
CHANGED
|
@@ -5,4 +5,8 @@ exports.session = require('./session');
|
|
|
5
5
|
exports.conversation = require('./conversation');
|
|
6
6
|
exports.project = require('./project');
|
|
7
7
|
exports.connector = require('./connector');
|
|
8
|
-
exports.
|
|
8
|
+
exports.file = require('./file');
|
|
9
|
+
exports.memory = require('./memory');
|
|
10
|
+
exports.settings = require('./settings');
|
|
11
|
+
exports.share = require('./share');
|
|
12
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const direct = require('../transport/direct/http-client');
|
|
3
|
+
const context = require('../runtime-context');
|
|
4
|
+
|
|
5
|
+
const requireOk = (response, target) => {
|
|
6
|
+
if (!response || response.status !== 200 || !response.json) {
|
|
7
|
+
throw Object.assign(new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`), { status: response?.status || null, target });
|
|
8
|
+
}
|
|
9
|
+
return response.json;
|
|
10
|
+
};
|
|
11
|
+
const requireSuccess = (response, target) => {
|
|
12
|
+
if (!response || response.status < 200 || response.status >= 300) {
|
|
13
|
+
throw Object.assign(new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`), { status: response?.status || null, target });
|
|
14
|
+
}
|
|
15
|
+
return response.json || { success: true };
|
|
16
|
+
};
|
|
17
|
+
const requiredId = (value, name) => {
|
|
18
|
+
if (typeof value !== 'string' || !value) throw Object.assign(new Error(`${name} is required`), { code: 'BAD_ARGS' });
|
|
19
|
+
return encodeURIComponent(value);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
exports.list = async (options = {}) => {
|
|
23
|
+
options = await context.bind(options);
|
|
24
|
+
const includeMemoryEntries = options.includeMemoryEntries === true;
|
|
25
|
+
const target = `/backend-api/memories?include_memory_entries=${includeMemoryEntries ? 'true' : 'false'}`;
|
|
26
|
+
return requireOk(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { route: '/backend-api/memories' }), target);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
exports.setStatus = async (options = {}) => {
|
|
30
|
+
options = await context.bind(options);
|
|
31
|
+
const memoryId = requiredId(options.memoryId, 'memoryId');
|
|
32
|
+
const status = typeof options.status === 'string' ? options.status.trim() : '';
|
|
33
|
+
if (!status) throw Object.assign(new Error('status is required'), { code: 'BAD_ARGS' });
|
|
34
|
+
const target = `/backend-api/memories/${memoryId}/status`;
|
|
35
|
+
return requireSuccess(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { method: 'PATCH', retry: false, route: '/backend-api/memories/{memory_id}/status', body: { status } }), target);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
exports.remove = async (options = {}) => {
|
|
39
|
+
options = await context.bind(options);
|
|
40
|
+
const memoryId = requiredId(options.memoryId, 'memoryId');
|
|
41
|
+
const target = `/backend-api/memories/${memoryId}`;
|
|
42
|
+
return requireSuccess(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { method: 'DELETE', retry: false, route: '/backend-api/memories/{memory_id}' }), target);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
exports.listHistory = async (options = {}) => {
|
|
46
|
+
options = await context.bind(options);
|
|
47
|
+
const limit = Number.isSafeInteger(options.limit) && options.limit > 0 ? options.limit : 50;
|
|
48
|
+
const target = `/backend-api/memories/history?limit=${limit}`;
|
|
49
|
+
return requireOk(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { route: '/backend-api/memories/history' }), target);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
exports.revertHistory = async (options = {}) => {
|
|
53
|
+
options = await context.bind(options);
|
|
54
|
+
const historyId = requiredId(options.historyId, 'historyId');
|
|
55
|
+
const target = `/backend-api/memories/history/${historyId}/revert`;
|
|
56
|
+
return requireSuccess(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { method: 'POST', retry: false, route: '/backend-api/memories/history/{memory_history_id}/revert', body: {} }), target);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
exports.removeHistory = async (options = {}) => {
|
|
60
|
+
options = await context.bind(options);
|
|
61
|
+
const historyId = requiredId(options.historyId, 'historyId');
|
|
62
|
+
const target = `/backend-api/memories/history/${historyId}`;
|
|
63
|
+
return requireSuccess(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { method: 'DELETE', retry: false, route: '/backend-api/memories/history/{memory_history_id}' }), target);
|
|
64
|
+
};
|
package/src/protocol/sse.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const stateOf = (events, completed, conversationId, currentNode, resumeToken, malformed) => ({
|
|
4
|
-
events: [...events], completed, conversationId, currentNode, resumeToken, malformed
|
|
4
|
+
events: [...events], completed, conversationId, currentNode, resumeToken, malformed,
|
|
5
|
+
resumeOffset: events.length
|
|
5
6
|
});
|
|
6
7
|
|
|
7
8
|
exports.createParser = () => {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const auth = require('./auth');
|
|
4
|
+
const paths = require('./config/paths');
|
|
5
|
+
|
|
6
|
+
const injectedTransport = (options) => !options.env && (typeof options.requestJson === 'function' || typeof options.requestText === 'function');
|
|
7
|
+
|
|
8
|
+
exports.bind = async (options = {}) => {
|
|
9
|
+
const resolved = { ...options };
|
|
10
|
+
if (resolved.token && resolved.sessionHeaders) return resolved;
|
|
11
|
+
if (injectedTransport(resolved)) return resolved;
|
|
12
|
+
const env = resolved.env || process.env;
|
|
13
|
+
if (!resolved.token) {
|
|
14
|
+
const current = await auth.ensure({ env });
|
|
15
|
+
resolved.token = current.accessToken;
|
|
16
|
+
}
|
|
17
|
+
if (!resolved.sessionHeaders) {
|
|
18
|
+
let state;
|
|
19
|
+
try { state = JSON.parse(fs.readFileSync(paths.sessionFile(env), 'utf8')); }
|
|
20
|
+
catch { throw Object.assign(new Error('ChatGPT session context required'), { code: 'SESSION_REQUIRED' }); }
|
|
21
|
+
if (!state?.headers || typeof state.headers !== 'object' || Array.isArray(state.headers)) {
|
|
22
|
+
throw Object.assign(new Error('ChatGPT session context required'), { code: 'SESSION_REQUIRED' });
|
|
23
|
+
}
|
|
24
|
+
resolved.sessionHeaders = { ...state.headers };
|
|
25
|
+
}
|
|
26
|
+
return resolved;
|
|
27
|
+
};
|