@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.
@@ -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 connector = require('../connector');
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, conduitToken, turnTraceId,
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
- if (state.completed) return { response: response || { status: 200, text: '' }, conversationId: observedId, currentNode: state.currentNode };
150
- if (!observedId) { if (upstreamError) throw upstreamError; throw Object.assign(new Error('conversation stream ended before conversation_id was available'), { code: 'STREAM_INCOMPLETE' }); }
151
- const streamState = await read.getConversationStreamStatus({
152
- conversationId: observedId, token, sessionHeaders, requestJson
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
- return { response: response || { status: 200, text: '' }, conversationId: observedId, currentNode: state.currentNode };
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
- const resumed = await read.resumeConversation({
160
- conversationId: observedId, offset: 0, conduitToken: state.resumeToken || conduitToken, turnTraceId,
161
- token, sessionHeaders, requestJson, requestText,
162
- onChunk: (chunk) => { resumedSawChunk = true; resumedProgress.push(chunk); options.onChunk?.(chunk); }
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
- if (!resumedState.completed) throw Object.assign(new Error('resumed conversation stream ended before [DONE]'), { code: 'STREAM_INCOMPLETE' });
167
- return { response: response || { status: 200, text: '' }, conversationId: observedId, currentNode: resumedState.currentNode || state.currentNode };
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 = false,
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: ใช้ sentinel headers จาก session เดิมถ้า fetch สดไม่สำเร็จ
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 = recoverStream
339
+ const sent = shouldRecoverStream
261
340
  ? await sendWithRecovery({
262
- target: sendTarget, options: sendOptions, conduitToken, turnTraceId: traceId,
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 = await read.getConversation({ conversationId, token, sessionHeaders, requestJson });
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 = false,
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: ใช้ sentinel headers จาก session เดิมถ้า fetch สดไม่สำเร็จ
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 = recoverStream
489
+ const sent = shouldRecoverStream
401
490
  ? await sendWithRecovery({
402
- target: sendTarget, options: sendOptions, conversationId, conduitToken, turnTraceId: traceId,
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 = await read.getConversation({ conversationId, token, sessionHeaders, requestJson });
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.api = require('./api');
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
+ };
@@ -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
+ };