@adminforth/completion-adapter-open-ai-chat-gpt 2.0.10 → 2.0.13

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