@adminforth/completion-adapter-open-ai-chat-gpt 2.0.11 → 2.0.14

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 (3) hide show
  1. package/dist/index.js +174 -70
  2. package/index.ts +209 -91
  3. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -8,48 +8,112 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { encoding_for_model } from "tiktoken";
11
+ function extractOutputText(data) {
12
+ var _a;
13
+ let text = "";
14
+ for (const item of (_a = data.output) !== null && _a !== void 0 ? _a : []) {
15
+ if (item.type !== "message" || !Array.isArray(item.content))
16
+ continue;
17
+ for (const part of item.content) {
18
+ if (part.type === "output_text" && typeof part.text === "string") {
19
+ text += part.text;
20
+ }
21
+ }
22
+ }
23
+ return text;
24
+ }
25
+ function extractReasoning(data) {
26
+ var _a, _b, _c;
27
+ let reasoning = "";
28
+ for (const item of (_a = data.output) !== null && _a !== void 0 ? _a : []) {
29
+ if (item.type !== "reasoning")
30
+ continue;
31
+ for (const part of (_b = item.summary) !== null && _b !== void 0 ? _b : []) {
32
+ if ((part === null || part === void 0 ? void 0 : part.type) === "summary_text" && typeof part.text === "string") {
33
+ reasoning += part.text;
34
+ }
35
+ }
36
+ if (!reasoning) {
37
+ for (const part of (_c = item.content) !== null && _c !== void 0 ? _c : []) {
38
+ if ((part === null || part === void 0 ? void 0 : part.type) === "reasoning_text" && typeof part.text === "string") {
39
+ reasoning += part.text;
40
+ }
41
+ }
42
+ }
43
+ }
44
+ return reasoning || undefined;
45
+ }
46
+ function parseSseBlock(block) {
47
+ let event;
48
+ let data = "";
49
+ for (const rawLine of block.split("\n")) {
50
+ const line = rawLine.trimEnd();
51
+ if (!line)
52
+ continue;
53
+ if (line.startsWith("event:"))
54
+ event = line.slice(6).trim();
55
+ if (line.startsWith("data:"))
56
+ data += line.slice(5).trim();
57
+ }
58
+ return data ? { event, data } : null;
59
+ }
11
60
  export default class CompletionAdapterOpenAIChatGPT {
12
61
  constructor(options) {
13
62
  //@ts-ignore
14
- this.complete = (content_1, ...args_1) => __awaiter(this, [content_1, ...args_1], void 0, function* (content, maxTokens = 50, outputSchema, onChunk) {
15
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
16
- // stop parameter is alredy not supported
17
- // adapter users should explicitely ask model to stop at dot if needed (or "Complete only up to the end of sentence")
63
+ this.complete = (content_1, ...args_1) => __awaiter(this, [content_1, ...args_1], void 0, function* (content, maxTokens = 50, outputSchema, reasoningEffort = "low", onChunk) {
64
+ var _a, _b, _c;
18
65
  const model = this.options.model || "gpt-5-nano";
19
66
  const isStreaming = typeof onChunk === "function";
20
- const resp = yield fetch("https://api.openai.com/v1/chat/completions", {
67
+ const body = {
68
+ model,
69
+ input: content,
70
+ max_output_tokens: maxTokens,
71
+ stream: isStreaming,
72
+ text: outputSchema
73
+ ? {
74
+ format: Object.assign({ type: "json_schema" }, outputSchema),
75
+ }
76
+ : {
77
+ format: {
78
+ type: "text",
79
+ },
80
+ },
81
+ reasoning: {
82
+ effort: reasoningEffort,
83
+ summary: "auto",
84
+ }
85
+ };
86
+ const resp = yield fetch("https://api.openai.com/v1/responses", {
21
87
  method: "POST",
22
88
  headers: {
23
89
  "Content-Type": "application/json",
24
90
  Authorization: `Bearer ${this.options.openAiApiKey}`,
25
91
  },
26
- body: JSON.stringify(Object.assign({ model, messages: [
27
- {
28
- role: "user",
29
- content,
30
- },
31
- ], max_completion_tokens: maxTokens, response_format: outputSchema
32
- ? Object.assign({ type: "json_schema" }, outputSchema) : undefined, stream: isStreaming }, this.options.extraRequestBodyParameters)),
92
+ body: JSON.stringify(body),
33
93
  });
34
94
  if (!resp.ok) {
35
95
  let errorMessage = `OpenAI request failed with status ${resp.status}`;
36
96
  try {
37
- const errorData = yield resp.json();
38
- if ((_a = errorData === null || errorData === void 0 ? void 0 : errorData.error) === null || _a === void 0 ? void 0 : _a.message) {
97
+ const errorData = (yield resp.json());
98
+ if ((_a = errorData.error) === null || _a === void 0 ? void 0 : _a.message)
39
99
  errorMessage = errorData.error.message;
40
- }
41
100
  }
42
- catch (_p) { }
101
+ catch (_d) { }
43
102
  return { error: errorMessage };
44
103
  }
45
104
  if (!isStreaming) {
46
- const data = yield resp.json();
105
+ const json = yield resp.json();
106
+ const data = json;
47
107
  if (data.error) {
48
108
  return { error: data.error.message };
49
109
  }
110
+ const parsedContent = extractOutputText(data);
111
+ const reasoning = extractReasoning(data);
50
112
  return {
51
- content: (_d = (_c = (_b = data.choices) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.message) === null || _d === void 0 ? void 0 : _d.content,
52
- finishReason: (_f = (_e = data.choices) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.finish_reason,
113
+ content: parsedContent,
114
+ finishReason: ((_b = data.incomplete_details) === null || _b === void 0 ? void 0 : _b.reason)
115
+ ? data.incomplete_details.reason
116
+ : undefined,
53
117
  };
54
118
  }
55
119
  if (!resp.body) {
@@ -59,79 +123,121 @@ export default class CompletionAdapterOpenAIChatGPT {
59
123
  const decoder = new TextDecoder("utf-8");
60
124
  let buffer = "";
61
125
  let fullContent = "";
126
+ let fullReasoning = "";
62
127
  let finishReason;
128
+ const handleEvent = (event, eventType) => __awaiter(this, void 0, void 0, function* () {
129
+ var _a, _b, _c, _d;
130
+ const type = (event === null || event === void 0 ? void 0 : event.type) || eventType;
131
+ if (type === "response.output_text.delta") {
132
+ const delta = (event === null || event === void 0 ? void 0 : event.delta) || "";
133
+ if (!delta)
134
+ return;
135
+ fullContent += delta;
136
+ yield (onChunk === null || onChunk === void 0 ? void 0 : onChunk(delta, { type: "output", delta, text: fullContent }));
137
+ return;
138
+ }
139
+ if (type === "response.reasoning_summary_text.delta" ||
140
+ type === "response.reasoning_text.delta") {
141
+ const delta = (event === null || event === void 0 ? void 0 : event.delta) || "";
142
+ if (!delta)
143
+ return;
144
+ fullReasoning += delta;
145
+ yield (onChunk === null || onChunk === void 0 ? void 0 : onChunk(delta, {
146
+ type: "reasoning",
147
+ delta,
148
+ text: fullReasoning,
149
+ }));
150
+ return;
151
+ }
152
+ if (type === "response.completed" ||
153
+ type === "response.incomplete") {
154
+ const response = event === null || event === void 0 ? void 0 : event.response;
155
+ if (!response)
156
+ return;
157
+ const finalContent = extractOutputText(response);
158
+ if (finalContent.startsWith(fullContent)) {
159
+ const delta = finalContent.slice(fullContent.length);
160
+ if (delta) {
161
+ fullContent = finalContent;
162
+ yield (onChunk === null || onChunk === void 0 ? void 0 : onChunk(delta, {
163
+ type: "output",
164
+ delta,
165
+ text: fullContent,
166
+ }));
167
+ }
168
+ }
169
+ const finalReasoning = extractReasoning(response) || "";
170
+ if (finalReasoning.startsWith(fullReasoning)) {
171
+ const delta = finalReasoning.slice(fullReasoning.length);
172
+ if (delta) {
173
+ fullReasoning = finalReasoning;
174
+ yield (onChunk === null || onChunk === void 0 ? void 0 : onChunk(delta, {
175
+ type: "reasoning",
176
+ delta,
177
+ text: fullReasoning,
178
+ }));
179
+ }
180
+ }
181
+ finishReason =
182
+ ((_a = response.incomplete_details) === null || _a === void 0 ? void 0 : _a.reason) || response.status || finishReason;
183
+ return;
184
+ }
185
+ if (type === "response.failed") {
186
+ throw new Error(((_c = (_b = event === null || event === void 0 ? void 0 : event.response) === null || _b === void 0 ? void 0 : _b.error) === null || _c === void 0 ? void 0 : _c.message) ||
187
+ ((_d = event === null || event === void 0 ? void 0 : event.error) === null || _d === void 0 ? void 0 : _d.message) ||
188
+ "Response failed");
189
+ }
190
+ });
63
191
  try {
64
192
  while (true) {
65
193
  const { value, done } = yield reader.read();
66
- if (done) {
194
+ if (done)
67
195
  break;
68
- }
69
196
  buffer += decoder.decode(value, { stream: true });
70
- const lines = buffer.split("\n");
71
- buffer = lines.pop() || "";
72
- for (const rawLine of lines) {
73
- const line = rawLine.trim();
74
- if (!line || !line.startsWith("data:")) {
197
+ const blocks = buffer.split("\n\n");
198
+ buffer = blocks.pop() || "";
199
+ for (const block of blocks) {
200
+ const parsedBlock = parseSseBlock(block);
201
+ if (!(parsedBlock === null || parsedBlock === void 0 ? void 0 : parsedBlock.data) || parsedBlock.data === "[DONE]")
75
202
  continue;
76
- }
77
- const dataStr = line.slice(5).trim();
78
- if (dataStr === "[DONE]") {
79
- return {
80
- content: fullContent,
81
- finishReason,
82
- };
83
- }
84
- let parsed;
203
+ let event;
85
204
  try {
86
- parsed = JSON.parse(dataStr);
205
+ event = JSON.parse(parsedBlock.data);
87
206
  }
88
- catch (_q) {
207
+ catch (_e) {
89
208
  continue;
90
209
  }
91
- if ((_g = parsed.error) === null || _g === void 0 ? void 0 : _g.message) {
92
- return { error: parsed.error.message };
210
+ if ((_c = event === null || event === void 0 ? void 0 : event.error) === null || _c === void 0 ? void 0 : _c.message) {
211
+ return { error: event.error.message };
93
212
  }
94
- const choice = (_h = parsed.choices) === null || _h === void 0 ? void 0 : _h[0];
95
- if (!choice) {
96
- continue;
97
- }
98
- if (choice.finish_reason) {
99
- finishReason = choice.finish_reason;
100
- }
101
- const chunk = (_k = (_j = choice.delta) === null || _j === void 0 ? void 0 : _j.content) !== null && _k !== void 0 ? _k : "";
102
- if (!chunk) {
103
- continue;
104
- }
105
- fullContent += chunk;
106
- yield onChunk(chunk);
213
+ yield handleEvent(event, parsedBlock.event);
107
214
  }
108
215
  }
109
- if (buffer.trim().startsWith("data:")) {
110
- const dataStr = buffer.trim().slice(5).trim();
111
- if (dataStr && dataStr !== "[DONE]") {
216
+ if (buffer.trim()) {
217
+ const parsedBlock = parseSseBlock(buffer.trim());
218
+ if ((parsedBlock === null || parsedBlock === void 0 ? void 0 : parsedBlock.data) && parsedBlock.data !== "[DONE]") {
112
219
  try {
113
- const parsed = JSON.parse(dataStr);
114
- const choice = (_l = parsed.choices) === null || _l === void 0 ? void 0 : _l[0];
115
- const chunk = (_o = (_m = choice === null || choice === void 0 ? void 0 : choice.delta) === null || _m === void 0 ? void 0 : _m.content) !== null && _o !== void 0 ? _o : "";
116
- if (chunk) {
117
- fullContent += chunk;
118
- yield onChunk(chunk);
119
- }
120
- if (choice === null || choice === void 0 ? void 0 : choice.finish_reason) {
121
- finishReason = choice.finish_reason;
122
- }
220
+ yield handleEvent(JSON.parse(parsedBlock.data), parsedBlock.event);
221
+ }
222
+ catch (error) {
223
+ return {
224
+ error: (error === null || error === void 0 ? void 0 : error.message) || "Streaming failed",
225
+ content: fullContent || undefined,
226
+ finishReason,
227
+ };
123
228
  }
124
- catch (_r) { }
125
229
  }
126
230
  }
127
231
  return {
128
- content: fullContent,
232
+ content: fullContent || undefined,
129
233
  finishReason,
130
234
  };
131
235
  }
132
236
  catch (error) {
133
237
  return {
134
238
  error: (error === null || error === void 0 ? void 0 : error.message) || "Streaming failed",
239
+ content: fullContent || undefined,
240
+ finishReason,
135
241
  };
136
242
  }
137
243
  finally {
@@ -147,8 +253,6 @@ export default class CompletionAdapterOpenAIChatGPT {
147
253
  }
148
254
  }
149
255
  measureTokensCount(content) {
150
- //TODO: Implement token counting logic
151
- const tokens = this.encoding.encode(content);
152
- return tokens.length;
256
+ return this.encoding.encode(content).length;
153
257
  }
154
258
  }
package/index.ts CHANGED
@@ -1,8 +1,76 @@
1
1
  import type { AdapterOptions } from "./types.js";
2
- import type { CompletionAdapter } from "adminforth";
2
+ import type { CompletionAdapter, CompletionStreamEvent } from "adminforth";
3
3
  import { encoding_for_model, type TiktokenModel } from "tiktoken";
4
+ import type OpenAI from "openai";
4
5
 
5
- type StreamChunkCallback = (chunk: string) => void | Promise<void>;
6
+ type StreamChunkCallback = (
7
+ chunk: string,
8
+ event?: CompletionStreamEvent,
9
+ ) => void | Promise<void>;
10
+
11
+ type ResponseCreateBody = OpenAI.Responses.ResponseCreateParams;
12
+ type OpenAIResponsesSuccess = OpenAI.Responses.Response;
13
+ type OpenAIErrorResponse = {
14
+ error?: {
15
+ message?: string;
16
+ type?: string;
17
+ param?: string | null;
18
+ code?: string | null;
19
+ };
20
+ };
21
+
22
+ function extractOutputText(data: OpenAIResponsesSuccess): string {
23
+ let text = "";
24
+
25
+ for (const item of data.output ?? []) {
26
+ if (item.type !== "message" || !Array.isArray(item.content)) continue;
27
+ for (const part of item.content) {
28
+ if (part.type === "output_text" && typeof part.text === "string") {
29
+ text += part.text;
30
+ }
31
+ }
32
+ }
33
+
34
+ return text;
35
+ }
36
+
37
+ function extractReasoning(data: OpenAIResponsesSuccess): string | undefined {
38
+ let reasoning = "";
39
+
40
+ for (const item of data.output ?? []) {
41
+ if (item.type !== "reasoning") continue;
42
+
43
+ for (const part of item.summary ?? []) {
44
+ if (part?.type === "summary_text" && typeof part.text === "string") {
45
+ reasoning += part.text;
46
+ }
47
+ }
48
+
49
+ if (!reasoning) {
50
+ for (const part of item.content ?? []) {
51
+ if (part?.type === "reasoning_text" && typeof part.text === "string") {
52
+ reasoning += part.text;
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ return reasoning || undefined;
59
+ }
60
+
61
+ function parseSseBlock(block: string) {
62
+ let event: string | undefined;
63
+ let data = "";
64
+
65
+ for (const rawLine of block.split("\n")) {
66
+ const line = rawLine.trimEnd();
67
+ if (!line) continue;
68
+ if (line.startsWith("event:")) event = line.slice(6).trim();
69
+ if (line.startsWith("data:")) data += line.slice(5).trim();
70
+ }
71
+
72
+ return data ? { event, data } : null;
73
+ }
6
74
 
7
75
  export default class CompletionAdapterOpenAIChatGPT
8
76
  implements CompletionAdapter
@@ -13,7 +81,7 @@ export default class CompletionAdapterOpenAIChatGPT
13
81
  constructor(options: AdapterOptions) {
14
82
  this.options = options;
15
83
  this.encoding = encoding_for_model(
16
- (this.options.model || "gpt-5-nano") as TiktokenModel
84
+ (this.options.model || "gpt-5-nano") as TiktokenModel,
17
85
  );
18
86
  }
19
87
 
@@ -24,72 +92,78 @@ export default class CompletionAdapterOpenAIChatGPT
24
92
  }
25
93
 
26
94
  measureTokensCount(content: string): number {
27
- //TODO: Implement token counting logic
28
- const tokens = this.encoding.encode(content);
29
- return tokens.length;
95
+ return this.encoding.encode(content).length;
30
96
  }
31
97
  //@ts-ignore
32
98
  complete = async (
33
99
  content: string,
34
- maxTokens: number = 50,
100
+ maxTokens = 50,
35
101
  outputSchema?: any,
102
+ reasoningEffort: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' = "low",
36
103
  onChunk?: StreamChunkCallback,
37
104
  ): Promise<{
38
105
  content?: string;
39
106
  finishReason?: string;
40
107
  error?: string;
41
108
  }> => {
42
- // stop parameter is alredy not supported
43
- // adapter users should explicitely ask model to stop at dot if needed (or "Complete only up to the end of sentence")
44
109
  const model = this.options.model || "gpt-5-nano";
45
110
  const isStreaming = typeof onChunk === "function";
111
+ const body = {
112
+ model,
113
+ input: content,
114
+ max_output_tokens: maxTokens,
115
+ stream: isStreaming,
116
+ text: outputSchema
117
+ ? {
118
+ format: {
119
+ type: "json_schema",
120
+ ...outputSchema,
121
+ },
122
+ }
123
+ : {
124
+ format: {
125
+ type: "text",
126
+ },
127
+ },
128
+ reasoning: {
129
+ effort: reasoningEffort,
130
+ summary: "auto",
131
+ }
132
+ } as ResponseCreateBody;
46
133
 
47
- const resp = await fetch("https://api.openai.com/v1/chat/completions", {
134
+ const resp = await fetch("https://api.openai.com/v1/responses", {
48
135
  method: "POST",
49
136
  headers: {
50
137
  "Content-Type": "application/json",
51
138
  Authorization: `Bearer ${this.options.openAiApiKey}`,
52
139
  },
53
- body: JSON.stringify({
54
- model,
55
- messages: [
56
- {
57
- role: "user",
58
- content,
59
- },
60
- ],
61
- max_completion_tokens: maxTokens,
62
- response_format: outputSchema
63
- ? {
64
- type: "json_schema",
65
- ...outputSchema,
66
- }
67
- : undefined,
68
- stream: isStreaming,
69
- ...this.options.extraRequestBodyParameters,
70
- }),
140
+ body: JSON.stringify(body),
71
141
  });
72
142
 
73
143
  if (!resp.ok) {
74
144
  let errorMessage = `OpenAI request failed with status ${resp.status}`;
75
145
  try {
76
- const errorData = await resp.json();
77
- if (errorData?.error?.message) {
78
- errorMessage = errorData.error.message;
79
- }
146
+ const errorData = (await resp.json()) as OpenAIErrorResponse;
147
+ if (errorData.error?.message) errorMessage = errorData.error.message;
80
148
  } catch {}
81
149
  return { error: errorMessage };
82
150
  }
83
151
 
84
152
  if (!isStreaming) {
85
- const data = await resp.json();
153
+ const json = await resp.json();
154
+ const data = json as OpenAIResponsesSuccess & OpenAIErrorResponse;
86
155
  if (data.error) {
87
156
  return { error: data.error.message };
88
157
  }
89
158
 
159
+ const parsedContent = extractOutputText(data);
160
+ const reasoning = extractReasoning(data);
161
+
90
162
  return {
91
- content: data.choices?.[0]?.message?.content,
92
- finishReason: data.choices?.[0]?.finish_reason,
163
+ content: parsedContent,
164
+ finishReason: data.incomplete_details?.reason
165
+ ? data.incomplete_details.reason
166
+ : undefined,
93
167
  };
94
168
  }
95
169
 
@@ -102,94 +176,138 @@ export default class CompletionAdapterOpenAIChatGPT
102
176
 
103
177
  let buffer = "";
104
178
  let fullContent = "";
179
+ let fullReasoning = "";
105
180
  let finishReason: string | undefined;
106
181
 
107
- try {
108
- while (true) {
109
- const { value, done } = await reader.read();
110
- if (done) {
111
- break;
112
- }
182
+ const handleEvent = async (event: any, eventType?: string) => {
183
+ const type = event?.type || eventType;
113
184
 
114
- buffer += decoder.decode(value, { stream: true });
185
+ if (type === "response.output_text.delta") {
186
+ const delta = event?.delta || "";
187
+ if (!delta) return;
188
+ fullContent += delta;
189
+ await onChunk?.(delta, { type: "output", delta, text: fullContent });
190
+ return;
191
+ }
115
192
 
116
- const lines = buffer.split("\n");
117
- buffer = lines.pop() || "";
193
+ if (
194
+ type === "response.reasoning_summary_text.delta" ||
195
+ type === "response.reasoning_text.delta"
196
+ ) {
197
+ const delta = event?.delta || "";
198
+ if (!delta) return;
199
+ fullReasoning += delta;
200
+ await onChunk?.(delta, {
201
+ type: "reasoning",
202
+ delta,
203
+ text: fullReasoning,
204
+ });
205
+ return;
206
+ }
118
207
 
119
- for (const rawLine of lines) {
120
- const line = rawLine.trim();
208
+ if (
209
+ type === "response.completed" ||
210
+ type === "response.incomplete"
211
+ ) {
212
+ const response = event?.response as OpenAIResponsesSuccess | undefined;
213
+ if (!response) return;
121
214
 
122
- if (!line || !line.startsWith("data:")) {
123
- continue;
215
+ const finalContent = extractOutputText(response);
216
+ if (finalContent.startsWith(fullContent)) {
217
+ const delta = finalContent.slice(fullContent.length);
218
+ if (delta) {
219
+ fullContent = finalContent;
220
+ await onChunk?.(delta, {
221
+ type: "output",
222
+ delta,
223
+ text: fullContent,
224
+ });
124
225
  }
226
+ }
125
227
 
126
- const dataStr = line.slice(5).trim();
127
-
128
- if (dataStr === "[DONE]") {
129
- return {
130
- content: fullContent,
131
- finishReason,
132
- };
228
+ const finalReasoning = extractReasoning(response) || "";
229
+ if (finalReasoning.startsWith(fullReasoning)) {
230
+ const delta = finalReasoning.slice(fullReasoning.length);
231
+ if (delta) {
232
+ fullReasoning = finalReasoning;
233
+ await onChunk?.(delta, {
234
+ type: "reasoning",
235
+ delta,
236
+ text: fullReasoning,
237
+ });
133
238
  }
239
+ }
134
240
 
135
- let parsed: any;
136
- try {
137
- parsed = JSON.parse(dataStr);
138
- } catch {
139
- continue;
140
- }
241
+ finishReason =
242
+ response.incomplete_details?.reason || response.status || finishReason;
243
+ return;
244
+ }
141
245
 
142
- if (parsed.error?.message) {
143
- return { error: parsed.error.message };
144
- }
246
+ if (type === "response.failed") {
247
+ throw new Error(
248
+ event?.response?.error?.message ||
249
+ event?.error?.message ||
250
+ "Response failed",
251
+ );
252
+ }
253
+ };
145
254
 
146
- const choice = parsed.choices?.[0];
147
- if (!choice) {
148
- continue;
149
- }
255
+ try {
256
+ while (true) {
257
+ const { value, done } = await reader.read();
258
+ if (done) break;
150
259
 
151
- if (choice.finish_reason) {
152
- finishReason = choice.finish_reason;
153
- }
260
+ buffer += decoder.decode(value, { stream: true });
261
+
262
+ const blocks = buffer.split("\n\n");
263
+ buffer = blocks.pop() || "";
264
+
265
+ for (const block of blocks) {
266
+ const parsedBlock = parseSseBlock(block);
267
+ if (!parsedBlock?.data || parsedBlock.data === "[DONE]") continue;
154
268
 
155
- const chunk = choice.delta?.content ?? "";
156
- if (!chunk) {
269
+ let event: any;
270
+ try {
271
+ event = JSON.parse(parsedBlock.data);
272
+ } catch {
157
273
  continue;
158
274
  }
159
275
 
160
- fullContent += chunk;
161
- await onChunk(chunk);
276
+ if (event?.error?.message) {
277
+ return { error: event.error.message };
278
+ }
279
+
280
+ await handleEvent(event, parsedBlock.event);
162
281
  }
163
282
  }
164
283
 
165
- if (buffer.trim().startsWith("data:")) {
166
- const dataStr = buffer.trim().slice(5).trim();
167
- if (dataStr && dataStr !== "[DONE]") {
284
+ if (buffer.trim()) {
285
+ const parsedBlock = parseSseBlock(buffer.trim());
286
+ if (parsedBlock?.data && parsedBlock.data !== "[DONE]") {
168
287
  try {
169
- const parsed = JSON.parse(dataStr);
170
- const choice = parsed.choices?.[0];
171
- const chunk = choice?.delta?.content ?? "";
172
- if (chunk) {
173
- fullContent += chunk;
174
- await onChunk(chunk);
175
- }
176
- if (choice?.finish_reason) {
177
- finishReason = choice.finish_reason;
178
- }
179
- } catch {}
288
+ await handleEvent(JSON.parse(parsedBlock.data), parsedBlock.event);
289
+ } catch (error: any) {
290
+ return {
291
+ error: error?.message || "Streaming failed",
292
+ content: fullContent || undefined,
293
+ finishReason,
294
+ };
295
+ }
180
296
  }
181
297
  }
182
298
 
183
299
  return {
184
- content: fullContent,
300
+ content: fullContent || undefined,
185
301
  finishReason,
186
302
  };
187
303
  } catch (error: any) {
188
304
  return {
189
305
  error: error?.message || "Streaming failed",
306
+ content: fullContent || undefined,
307
+ finishReason,
190
308
  };
191
309
  } finally {
192
310
  reader.releaseLock();
193
311
  }
194
312
  };
195
- }
313
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/completion-adapter-open-ai-chat-gpt",
3
- "version": "2.0.11",
3
+ "version": "2.0.14",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -16,6 +16,7 @@
16
16
  "typescript": "^5.9.3"
17
17
  },
18
18
  "dependencies": {
19
+ "openai": "^6.34.0",
19
20
  "tiktoken": "^1.0.22"
20
21
  },
21
22
  "peerDependencies": {