@adminforth/completion-adapter-openai-responses 2.0.21
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/Changelog.md +6 -0
- package/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +339 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.js +1 -0
- package/index.ts +418 -0
- package/package.json +25 -0
- package/tsconfig.json +14 -0
- package/types.ts +19 -0
package/Changelog.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Devforth.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @adminforth/completion-adapter-openai-responses
|
|
2
|
+
|
|
3
|
+
AdminForth completion adapter for the OpenAI Responses API.
|
|
4
|
+
|
|
5
|
+
This package is the fully compatible successor to `@adminforth/completion-adapter-open-ai-chat-gpt`.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm i @adminforth/completion-adapter-openai-responses
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import CompletionAdapterOpenAIResponses from "@adminforth/completion-adapter-openai-responses";
|
|
17
|
+
|
|
18
|
+
const adapter = new CompletionAdapterOpenAIResponses({
|
|
19
|
+
openAiApiKey: process.env.OPENAI_API_KEY as string,
|
|
20
|
+
model: "gpt-5-nano",
|
|
21
|
+
extraRequestBodyParameters: {
|
|
22
|
+
temperature: 0.7,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The adapter supports:
|
|
28
|
+
|
|
29
|
+
- regular text completion
|
|
30
|
+
- `json_schema` structured output
|
|
31
|
+
- reasoning effort control
|
|
32
|
+
- tool calls
|
|
33
|
+
- streaming output chunks
|
|
34
|
+
- streaming reasoning chunks
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AdapterOptions } from "./types.js";
|
|
2
|
+
import type { CompletionAdapter, CompletionStreamEvent, CompletionTool } from "adminforth";
|
|
3
|
+
export type { AdapterOptions } from "./types.js";
|
|
4
|
+
type StreamChunkCallback = (chunk: string, event?: CompletionStreamEvent) => void | Promise<void>;
|
|
5
|
+
export default class CompletionAdapterOpenAIResponses implements CompletionAdapter {
|
|
6
|
+
options: AdapterOptions;
|
|
7
|
+
private encoding;
|
|
8
|
+
constructor(options: AdapterOptions);
|
|
9
|
+
validate(): void;
|
|
10
|
+
measureTokensCount(content: string): number;
|
|
11
|
+
complete: (content: string, maxTokens?: number, outputSchema?: any, reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh", toolsOrOnChunk?: CompletionTool[] | StreamChunkCallback, onChunk?: StreamChunkCallback) => Promise<{
|
|
12
|
+
content?: string;
|
|
13
|
+
finishReason?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
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 extractFunctionCall(data) {
|
|
47
|
+
var _a;
|
|
48
|
+
for (const item of (_a = data.output) !== null && _a !== void 0 ? _a : []) {
|
|
49
|
+
if (item.type === "function_call") {
|
|
50
|
+
return item;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function executeToolCall(toolCall, tools) {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
const tool = tools === null || tools === void 0 ? void 0 : tools.find((candidate) => candidate.name === toolCall.name);
|
|
58
|
+
if (!tool) {
|
|
59
|
+
throw new Error(`Tool "${toolCall.name}" not found`);
|
|
60
|
+
}
|
|
61
|
+
const toolResult = yield tool.handler(JSON.parse(toolCall.arguments));
|
|
62
|
+
if (typeof toolResult === "string")
|
|
63
|
+
return toolResult;
|
|
64
|
+
if (typeof toolResult === "undefined")
|
|
65
|
+
return "";
|
|
66
|
+
return JSON.stringify(toolResult);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function parseSseBlock(block) {
|
|
70
|
+
let event;
|
|
71
|
+
let data = "";
|
|
72
|
+
for (const rawLine of block.split("\n")) {
|
|
73
|
+
const line = rawLine.trimEnd();
|
|
74
|
+
if (!line)
|
|
75
|
+
continue;
|
|
76
|
+
if (line.startsWith("event:"))
|
|
77
|
+
event = line.slice(6).trim();
|
|
78
|
+
if (line.startsWith("data:"))
|
|
79
|
+
data += line.slice(5).trim();
|
|
80
|
+
}
|
|
81
|
+
return data ? { event, data } : null;
|
|
82
|
+
}
|
|
83
|
+
export default class CompletionAdapterOpenAIResponses {
|
|
84
|
+
constructor(options) {
|
|
85
|
+
this.complete = (content_1, ...args_1) => __awaiter(this, [content_1, ...args_1], void 0, function* (content, maxTokens = 50, outputSchema, reasoningEffort = "low", toolsOrOnChunk, onChunk) {
|
|
86
|
+
var _a, _b, _c;
|
|
87
|
+
const model = this.options.model || "gpt-5-nano";
|
|
88
|
+
const tools = Array.isArray(toolsOrOnChunk) ? toolsOrOnChunk : undefined;
|
|
89
|
+
const streamChunkCallback = typeof toolsOrOnChunk === "function" ? toolsOrOnChunk : onChunk;
|
|
90
|
+
const isStreaming = typeof streamChunkCallback === "function";
|
|
91
|
+
const extra = this.options.extraRequestBodyParameters;
|
|
92
|
+
let openAiTools = undefined;
|
|
93
|
+
if (tools && tools.length > 0) {
|
|
94
|
+
openAiTools = tools.map((tool) => ({
|
|
95
|
+
type: "function",
|
|
96
|
+
name: tool.name,
|
|
97
|
+
description: tool.description,
|
|
98
|
+
parameters: tool.input_schema,
|
|
99
|
+
strict: true,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
const body = Object.assign({ model, input: content, max_output_tokens: maxTokens, stream: isStreaming, text: outputSchema
|
|
103
|
+
? {
|
|
104
|
+
format: Object.assign({ type: "json_schema" }, outputSchema),
|
|
105
|
+
}
|
|
106
|
+
: {
|
|
107
|
+
format: {
|
|
108
|
+
type: "text",
|
|
109
|
+
},
|
|
110
|
+
}, reasoning: {
|
|
111
|
+
effort: reasoningEffort,
|
|
112
|
+
summary: "auto",
|
|
113
|
+
}, tools: openAiTools }, extra);
|
|
114
|
+
const resp = yield fetch("https://api.openai.com/v1/responses", {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: {
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
Authorization: `Bearer ${this.options.openAiApiKey}`,
|
|
119
|
+
},
|
|
120
|
+
body: JSON.stringify(body),
|
|
121
|
+
});
|
|
122
|
+
if (!resp.ok) {
|
|
123
|
+
let errorMessage = `OpenAI request failed with status ${resp.status}`;
|
|
124
|
+
try {
|
|
125
|
+
const errorData = (yield resp.json());
|
|
126
|
+
if ((_a = errorData.error) === null || _a === void 0 ? void 0 : _a.message)
|
|
127
|
+
errorMessage = errorData.error.message;
|
|
128
|
+
}
|
|
129
|
+
catch (_d) { }
|
|
130
|
+
return { error: errorMessage };
|
|
131
|
+
}
|
|
132
|
+
if (!isStreaming) {
|
|
133
|
+
const json = yield resp.json();
|
|
134
|
+
const data = json;
|
|
135
|
+
if (data.error) {
|
|
136
|
+
return { error: data.error.message };
|
|
137
|
+
}
|
|
138
|
+
const toolCall = extractFunctionCall(data);
|
|
139
|
+
if (toolCall) {
|
|
140
|
+
try {
|
|
141
|
+
const toolResult = yield executeToolCall(toolCall, tools);
|
|
142
|
+
return {
|
|
143
|
+
content: toolResult,
|
|
144
|
+
finishReason: "tool_call",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
return {
|
|
149
|
+
error: (error === null || error === void 0 ? void 0 : error.message) || "Tool execution failed",
|
|
150
|
+
finishReason: "tool_call",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const parsedContent = extractOutputText(data);
|
|
155
|
+
return {
|
|
156
|
+
content: parsedContent,
|
|
157
|
+
finishReason: ((_b = data.incomplete_details) === null || _b === void 0 ? void 0 : _b.reason)
|
|
158
|
+
? data.incomplete_details.reason
|
|
159
|
+
: undefined,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (!resp.body) {
|
|
163
|
+
return { error: "Response body is empty" };
|
|
164
|
+
}
|
|
165
|
+
const reader = resp.body.getReader();
|
|
166
|
+
const decoder = new TextDecoder("utf-8");
|
|
167
|
+
let buffer = "";
|
|
168
|
+
let fullContent = "";
|
|
169
|
+
let fullReasoning = "";
|
|
170
|
+
let finishReason;
|
|
171
|
+
let completedResponse;
|
|
172
|
+
const handleEvent = (event, eventType) => __awaiter(this, void 0, void 0, function* () {
|
|
173
|
+
var _a, _b, _c, _d;
|
|
174
|
+
const type = (event === null || event === void 0 ? void 0 : event.type) || eventType;
|
|
175
|
+
if (type === "response.output_text.delta") {
|
|
176
|
+
const delta = (event === null || event === void 0 ? void 0 : event.delta) || "";
|
|
177
|
+
if (!delta)
|
|
178
|
+
return;
|
|
179
|
+
fullContent += delta;
|
|
180
|
+
yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, { type: "output", delta, text: fullContent }));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (type === "response.reasoning_summary_text.delta" ||
|
|
184
|
+
type === "response.reasoning_text.delta") {
|
|
185
|
+
const delta = (event === null || event === void 0 ? void 0 : event.delta) || "";
|
|
186
|
+
if (!delta)
|
|
187
|
+
return;
|
|
188
|
+
fullReasoning += delta;
|
|
189
|
+
yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
|
|
190
|
+
type: "reasoning",
|
|
191
|
+
delta,
|
|
192
|
+
text: fullReasoning,
|
|
193
|
+
}));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (type === "response.completed" || type === "response.incomplete") {
|
|
197
|
+
const response = event === null || event === void 0 ? void 0 : event.response;
|
|
198
|
+
if (!response)
|
|
199
|
+
return;
|
|
200
|
+
const finalContent = extractOutputText(response);
|
|
201
|
+
if (finalContent.startsWith(fullContent)) {
|
|
202
|
+
const delta = finalContent.slice(fullContent.length);
|
|
203
|
+
if (delta) {
|
|
204
|
+
fullContent = finalContent;
|
|
205
|
+
yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
|
|
206
|
+
type: "output",
|
|
207
|
+
delta,
|
|
208
|
+
text: fullContent,
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const finalReasoning = extractReasoning(response) || "";
|
|
213
|
+
if (finalReasoning.startsWith(fullReasoning)) {
|
|
214
|
+
const delta = finalReasoning.slice(fullReasoning.length);
|
|
215
|
+
if (delta) {
|
|
216
|
+
fullReasoning = finalReasoning;
|
|
217
|
+
yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
|
|
218
|
+
type: "reasoning",
|
|
219
|
+
delta,
|
|
220
|
+
text: fullReasoning,
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
finishReason =
|
|
225
|
+
((_a = response.incomplete_details) === null || _a === void 0 ? void 0 : _a.reason) || response.status || finishReason;
|
|
226
|
+
completedResponse = response;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (type === "response.failed") {
|
|
230
|
+
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) ||
|
|
231
|
+
((_d = event === null || event === void 0 ? void 0 : event.error) === null || _d === void 0 ? void 0 : _d.message) ||
|
|
232
|
+
"Response failed");
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
try {
|
|
236
|
+
while (true) {
|
|
237
|
+
const { value, done } = yield reader.read();
|
|
238
|
+
if (done)
|
|
239
|
+
break;
|
|
240
|
+
buffer += decoder.decode(value, { stream: true });
|
|
241
|
+
const blocks = buffer.split("\n\n");
|
|
242
|
+
buffer = blocks.pop() || "";
|
|
243
|
+
for (const block of blocks) {
|
|
244
|
+
const parsedBlock = parseSseBlock(block);
|
|
245
|
+
if (!(parsedBlock === null || parsedBlock === void 0 ? void 0 : parsedBlock.data) || parsedBlock.data === "[DONE]")
|
|
246
|
+
continue;
|
|
247
|
+
let event;
|
|
248
|
+
try {
|
|
249
|
+
event = JSON.parse(parsedBlock.data);
|
|
250
|
+
}
|
|
251
|
+
catch (_e) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if ((_c = event === null || event === void 0 ? void 0 : event.error) === null || _c === void 0 ? void 0 : _c.message) {
|
|
255
|
+
return { error: event.error.message };
|
|
256
|
+
}
|
|
257
|
+
yield handleEvent(event, parsedBlock.event);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (buffer.trim()) {
|
|
261
|
+
const parsedBlock = parseSseBlock(buffer.trim());
|
|
262
|
+
if ((parsedBlock === null || parsedBlock === void 0 ? void 0 : parsedBlock.data) && parsedBlock.data !== "[DONE]") {
|
|
263
|
+
try {
|
|
264
|
+
yield handleEvent(JSON.parse(parsedBlock.data), parsedBlock.event);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
return {
|
|
268
|
+
error: (error === null || error === void 0 ? void 0 : error.message) || "Streaming failed",
|
|
269
|
+
content: fullContent || undefined,
|
|
270
|
+
finishReason,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (completedResponse) {
|
|
276
|
+
const toolCall = extractFunctionCall(completedResponse);
|
|
277
|
+
if (toolCall) {
|
|
278
|
+
try {
|
|
279
|
+
const toolResult = yield executeToolCall(toolCall, tools);
|
|
280
|
+
if (toolResult) {
|
|
281
|
+
const delta = toolResult.startsWith(fullContent)
|
|
282
|
+
? toolResult.slice(fullContent.length)
|
|
283
|
+
: toolResult;
|
|
284
|
+
if (delta) {
|
|
285
|
+
yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
|
|
286
|
+
type: "output",
|
|
287
|
+
delta,
|
|
288
|
+
text: toolResult,
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
content: toolResult,
|
|
294
|
+
finishReason: "tool_call",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
return {
|
|
299
|
+
error: (error === null || error === void 0 ? void 0 : error.message) || "Tool execution failed",
|
|
300
|
+
content: fullContent || undefined,
|
|
301
|
+
finishReason: "tool_call",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
content: fullContent || undefined,
|
|
308
|
+
finishReason,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
return {
|
|
313
|
+
error: (error === null || error === void 0 ? void 0 : error.message) || "Streaming failed",
|
|
314
|
+
content: fullContent || undefined,
|
|
315
|
+
finishReason,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
reader.releaseLock();
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
this.options = options;
|
|
323
|
+
try {
|
|
324
|
+
this.encoding = encoding_for_model((this.options.model || "gpt-5-nano"));
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
console.warn(`Failed to initialize tiktoken tokenizer for model "${this.options.model}", falling back to "gpt-5-nano". Error:`);
|
|
328
|
+
this.encoding = encoding_for_model("gpt-5-nano");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
validate() {
|
|
332
|
+
if (!this.options.openAiApiKey) {
|
|
333
|
+
throw new Error("openAiApiKey is required");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
measureTokensCount(content) {
|
|
337
|
+
return this.encoding.encode(content).length;
|
|
338
|
+
}
|
|
339
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface AdapterOptions {
|
|
2
|
+
/**
|
|
3
|
+
* OpenAI API key. Go to https://platform.openai.com/, go to Dashboard -> API keys -> Create new secret key
|
|
4
|
+
* Paste value in your .env file OPENAI_API_KEY=your_key
|
|
5
|
+
* Set openAiApiKey: process.env.OPENAI_API_KEY to access it
|
|
6
|
+
*/
|
|
7
|
+
openAiApiKey: string;
|
|
8
|
+
/**
|
|
9
|
+
* Model name. Go to https://platform.openai.com/docs/models, select model and copy name.
|
|
10
|
+
* Default is `gpt-5-nano`.
|
|
11
|
+
*/
|
|
12
|
+
model?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Additional request body parameters to include in the API request.
|
|
15
|
+
*/
|
|
16
|
+
extraRequestBodyParameters?: Record<string, unknown>;
|
|
17
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/index.ts
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import type { AdapterOptions } from "./types.js";
|
|
2
|
+
import type { CompletionAdapter, CompletionStreamEvent, CompletionTool } from "adminforth";
|
|
3
|
+
import { encoding_for_model, type TiktokenModel } from "tiktoken";
|
|
4
|
+
import type OpenAI from "openai";
|
|
5
|
+
|
|
6
|
+
export type { AdapterOptions } from "./types.js";
|
|
7
|
+
|
|
8
|
+
type StreamChunkCallback = (
|
|
9
|
+
chunk: string,
|
|
10
|
+
event?: CompletionStreamEvent,
|
|
11
|
+
) => void | Promise<void>;
|
|
12
|
+
|
|
13
|
+
type ResponseCreateBody = OpenAI.Responses.ResponseCreateParams;
|
|
14
|
+
type OpenAIResponsesSuccess = OpenAI.Responses.Response;
|
|
15
|
+
type OpenAIErrorResponse = {
|
|
16
|
+
error?: {
|
|
17
|
+
message?: string;
|
|
18
|
+
type?: string;
|
|
19
|
+
param?: string | null;
|
|
20
|
+
code?: string | null;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
type OpenAITool = OpenAI.Responses.Tool;
|
|
24
|
+
type OpenAIFunctionCall = Extract<
|
|
25
|
+
OpenAI.Responses.ResponseOutputItem,
|
|
26
|
+
{ type: "function_call" }
|
|
27
|
+
>;
|
|
28
|
+
|
|
29
|
+
function extractOutputText(data: OpenAIResponsesSuccess): string {
|
|
30
|
+
let text = "";
|
|
31
|
+
|
|
32
|
+
for (const item of data.output ?? []) {
|
|
33
|
+
if (item.type !== "message" || !Array.isArray(item.content)) continue;
|
|
34
|
+
for (const part of item.content) {
|
|
35
|
+
if (part.type === "output_text" && typeof part.text === "string") {
|
|
36
|
+
text += part.text;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function extractReasoning(data: OpenAIResponsesSuccess): string | undefined {
|
|
45
|
+
let reasoning = "";
|
|
46
|
+
|
|
47
|
+
for (const item of data.output ?? []) {
|
|
48
|
+
if (item.type !== "reasoning") continue;
|
|
49
|
+
|
|
50
|
+
for (const part of item.summary ?? []) {
|
|
51
|
+
if (part?.type === "summary_text" && typeof part.text === "string") {
|
|
52
|
+
reasoning += part.text;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!reasoning) {
|
|
57
|
+
for (const part of item.content ?? []) {
|
|
58
|
+
if (part?.type === "reasoning_text" && typeof part.text === "string") {
|
|
59
|
+
reasoning += part.text;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return reasoning || undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractFunctionCall(
|
|
69
|
+
data: OpenAIResponsesSuccess,
|
|
70
|
+
): OpenAIFunctionCall | undefined {
|
|
71
|
+
for (const item of data.output ?? []) {
|
|
72
|
+
if (item.type === "function_call") {
|
|
73
|
+
return item;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function executeToolCall(
|
|
81
|
+
toolCall: OpenAIFunctionCall,
|
|
82
|
+
tools?: CompletionTool[],
|
|
83
|
+
): Promise<string> {
|
|
84
|
+
const tool = tools?.find((candidate) => candidate.name === toolCall.name);
|
|
85
|
+
if (!tool) {
|
|
86
|
+
throw new Error(`Tool "${toolCall.name}" not found`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const toolResult = await tool.handler(JSON.parse(toolCall.arguments));
|
|
90
|
+
if (typeof toolResult === "string") return toolResult;
|
|
91
|
+
if (typeof toolResult === "undefined") return "";
|
|
92
|
+
return JSON.stringify(toolResult);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseSseBlock(block: string) {
|
|
96
|
+
let event: string | undefined;
|
|
97
|
+
let data = "";
|
|
98
|
+
|
|
99
|
+
for (const rawLine of block.split("\n")) {
|
|
100
|
+
const line = rawLine.trimEnd();
|
|
101
|
+
if (!line) continue;
|
|
102
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
103
|
+
if (line.startsWith("data:")) data += line.slice(5).trim();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return data ? { event, data } : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default class CompletionAdapterOpenAIResponses
|
|
110
|
+
implements CompletionAdapter
|
|
111
|
+
{
|
|
112
|
+
options: AdapterOptions;
|
|
113
|
+
private encoding: ReturnType<typeof encoding_for_model>;
|
|
114
|
+
|
|
115
|
+
constructor(options: AdapterOptions) {
|
|
116
|
+
this.options = options;
|
|
117
|
+
try {
|
|
118
|
+
this.encoding = encoding_for_model(
|
|
119
|
+
(this.options.model || "gpt-5-nano") as TiktokenModel,
|
|
120
|
+
);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.warn(
|
|
123
|
+
`Failed to initialize tiktoken tokenizer for model "${this.options.model}", falling back to "gpt-5-nano". Error:`,
|
|
124
|
+
);
|
|
125
|
+
this.encoding = encoding_for_model("gpt-5-nano" as TiktokenModel);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
validate() {
|
|
130
|
+
if (!this.options.openAiApiKey) {
|
|
131
|
+
throw new Error("openAiApiKey is required");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
measureTokensCount(content: string): number {
|
|
136
|
+
return this.encoding.encode(content).length;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
complete = async (
|
|
140
|
+
content: string,
|
|
141
|
+
maxTokens = 50,
|
|
142
|
+
outputSchema?: any,
|
|
143
|
+
reasoningEffort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" = "low",
|
|
144
|
+
toolsOrOnChunk?: CompletionTool[] | StreamChunkCallback,
|
|
145
|
+
onChunk?: StreamChunkCallback,
|
|
146
|
+
): Promise<{
|
|
147
|
+
content?: string;
|
|
148
|
+
finishReason?: string;
|
|
149
|
+
error?: string;
|
|
150
|
+
}> => {
|
|
151
|
+
const model = this.options.model || "gpt-5-nano";
|
|
152
|
+
const tools = Array.isArray(toolsOrOnChunk) ? toolsOrOnChunk : undefined;
|
|
153
|
+
const streamChunkCallback =
|
|
154
|
+
typeof toolsOrOnChunk === "function" ? toolsOrOnChunk : onChunk;
|
|
155
|
+
const isStreaming = typeof streamChunkCallback === "function";
|
|
156
|
+
const extra = this.options.extraRequestBodyParameters;
|
|
157
|
+
let openAiTools: OpenAITool[] | undefined = undefined;
|
|
158
|
+
if (tools && tools.length > 0) {
|
|
159
|
+
openAiTools = tools.map((tool) => ({
|
|
160
|
+
type: "function",
|
|
161
|
+
name: tool.name,
|
|
162
|
+
description: tool.description,
|
|
163
|
+
parameters: tool.input_schema,
|
|
164
|
+
strict: true,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const body = {
|
|
169
|
+
model,
|
|
170
|
+
input: content,
|
|
171
|
+
max_output_tokens: maxTokens,
|
|
172
|
+
stream: isStreaming,
|
|
173
|
+
text: outputSchema
|
|
174
|
+
? {
|
|
175
|
+
format: {
|
|
176
|
+
type: "json_schema",
|
|
177
|
+
...outputSchema,
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
: {
|
|
181
|
+
format: {
|
|
182
|
+
type: "text",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
reasoning: {
|
|
186
|
+
effort: reasoningEffort,
|
|
187
|
+
summary: "auto",
|
|
188
|
+
},
|
|
189
|
+
tools: openAiTools,
|
|
190
|
+
...extra,
|
|
191
|
+
} as ResponseCreateBody;
|
|
192
|
+
|
|
193
|
+
const resp = await fetch("https://api.openai.com/v1/responses", {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: {
|
|
196
|
+
"Content-Type": "application/json",
|
|
197
|
+
Authorization: `Bearer ${this.options.openAiApiKey}`,
|
|
198
|
+
},
|
|
199
|
+
body: JSON.stringify(body),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!resp.ok) {
|
|
203
|
+
let errorMessage = `OpenAI request failed with status ${resp.status}`;
|
|
204
|
+
try {
|
|
205
|
+
const errorData = (await resp.json()) as OpenAIErrorResponse;
|
|
206
|
+
if (errorData.error?.message) errorMessage = errorData.error.message;
|
|
207
|
+
} catch {}
|
|
208
|
+
return { error: errorMessage };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!isStreaming) {
|
|
212
|
+
const json = await resp.json();
|
|
213
|
+
const data = json as OpenAIResponsesSuccess & OpenAIErrorResponse;
|
|
214
|
+
if (data.error) {
|
|
215
|
+
return { error: data.error.message };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const toolCall = extractFunctionCall(data);
|
|
219
|
+
if (toolCall) {
|
|
220
|
+
try {
|
|
221
|
+
const toolResult = await executeToolCall(toolCall, tools);
|
|
222
|
+
return {
|
|
223
|
+
content: toolResult,
|
|
224
|
+
finishReason: "tool_call",
|
|
225
|
+
};
|
|
226
|
+
} catch (error: any) {
|
|
227
|
+
return {
|
|
228
|
+
error: error?.message || "Tool execution failed",
|
|
229
|
+
finishReason: "tool_call",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const parsedContent = extractOutputText(data);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
content: parsedContent,
|
|
238
|
+
finishReason: data.incomplete_details?.reason
|
|
239
|
+
? data.incomplete_details.reason
|
|
240
|
+
: undefined,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!resp.body) {
|
|
245
|
+
return { error: "Response body is empty" };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const reader = resp.body.getReader();
|
|
249
|
+
const decoder = new TextDecoder("utf-8");
|
|
250
|
+
|
|
251
|
+
let buffer = "";
|
|
252
|
+
let fullContent = "";
|
|
253
|
+
let fullReasoning = "";
|
|
254
|
+
let finishReason: string | undefined;
|
|
255
|
+
let completedResponse: OpenAIResponsesSuccess | undefined;
|
|
256
|
+
|
|
257
|
+
const handleEvent = async (event: any, eventType?: string) => {
|
|
258
|
+
const type = event?.type || eventType;
|
|
259
|
+
|
|
260
|
+
if (type === "response.output_text.delta") {
|
|
261
|
+
const delta = event?.delta || "";
|
|
262
|
+
if (!delta) return;
|
|
263
|
+
fullContent += delta;
|
|
264
|
+
await streamChunkCallback?.(delta, { type: "output", delta, text: fullContent });
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (
|
|
269
|
+
type === "response.reasoning_summary_text.delta" ||
|
|
270
|
+
type === "response.reasoning_text.delta"
|
|
271
|
+
) {
|
|
272
|
+
const delta = event?.delta || "";
|
|
273
|
+
if (!delta) return;
|
|
274
|
+
fullReasoning += delta;
|
|
275
|
+
await streamChunkCallback?.(delta, {
|
|
276
|
+
type: "reasoning",
|
|
277
|
+
delta,
|
|
278
|
+
text: fullReasoning,
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (type === "response.completed" || type === "response.incomplete") {
|
|
284
|
+
const response = event?.response as OpenAIResponsesSuccess | undefined;
|
|
285
|
+
if (!response) return;
|
|
286
|
+
|
|
287
|
+
const finalContent = extractOutputText(response);
|
|
288
|
+
if (finalContent.startsWith(fullContent)) {
|
|
289
|
+
const delta = finalContent.slice(fullContent.length);
|
|
290
|
+
if (delta) {
|
|
291
|
+
fullContent = finalContent;
|
|
292
|
+
await streamChunkCallback?.(delta, {
|
|
293
|
+
type: "output",
|
|
294
|
+
delta,
|
|
295
|
+
text: fullContent,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const finalReasoning = extractReasoning(response) || "";
|
|
301
|
+
if (finalReasoning.startsWith(fullReasoning)) {
|
|
302
|
+
const delta = finalReasoning.slice(fullReasoning.length);
|
|
303
|
+
if (delta) {
|
|
304
|
+
fullReasoning = finalReasoning;
|
|
305
|
+
await streamChunkCallback?.(delta, {
|
|
306
|
+
type: "reasoning",
|
|
307
|
+
delta,
|
|
308
|
+
text: fullReasoning,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
finishReason =
|
|
314
|
+
response.incomplete_details?.reason || response.status || finishReason;
|
|
315
|
+
completedResponse = response;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (type === "response.failed") {
|
|
320
|
+
throw new Error(
|
|
321
|
+
event?.response?.error?.message ||
|
|
322
|
+
event?.error?.message ||
|
|
323
|
+
"Response failed",
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
while (true) {
|
|
330
|
+
const { value, done } = await reader.read();
|
|
331
|
+
if (done) break;
|
|
332
|
+
|
|
333
|
+
buffer += decoder.decode(value, { stream: true });
|
|
334
|
+
|
|
335
|
+
const blocks = buffer.split("\n\n");
|
|
336
|
+
buffer = blocks.pop() || "";
|
|
337
|
+
|
|
338
|
+
for (const block of blocks) {
|
|
339
|
+
const parsedBlock = parseSseBlock(block);
|
|
340
|
+
if (!parsedBlock?.data || parsedBlock.data === "[DONE]") continue;
|
|
341
|
+
|
|
342
|
+
let event: any;
|
|
343
|
+
try {
|
|
344
|
+
event = JSON.parse(parsedBlock.data);
|
|
345
|
+
} catch {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (event?.error?.message) {
|
|
350
|
+
return { error: event.error.message };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
await handleEvent(event, parsedBlock.event);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (buffer.trim()) {
|
|
358
|
+
const parsedBlock = parseSseBlock(buffer.trim());
|
|
359
|
+
if (parsedBlock?.data && parsedBlock.data !== "[DONE]") {
|
|
360
|
+
try {
|
|
361
|
+
await handleEvent(JSON.parse(parsedBlock.data), parsedBlock.event);
|
|
362
|
+
} catch (error: any) {
|
|
363
|
+
return {
|
|
364
|
+
error: error?.message || "Streaming failed",
|
|
365
|
+
content: fullContent || undefined,
|
|
366
|
+
finishReason,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (completedResponse) {
|
|
373
|
+
const toolCall = extractFunctionCall(completedResponse);
|
|
374
|
+
if (toolCall) {
|
|
375
|
+
try {
|
|
376
|
+
const toolResult = await executeToolCall(toolCall, tools);
|
|
377
|
+
if (toolResult) {
|
|
378
|
+
const delta = toolResult.startsWith(fullContent)
|
|
379
|
+
? toolResult.slice(fullContent.length)
|
|
380
|
+
: toolResult;
|
|
381
|
+
if (delta) {
|
|
382
|
+
await streamChunkCallback?.(delta, {
|
|
383
|
+
type: "output",
|
|
384
|
+
delta,
|
|
385
|
+
text: toolResult,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
content: toolResult,
|
|
392
|
+
finishReason: "tool_call",
|
|
393
|
+
};
|
|
394
|
+
} catch (error: any) {
|
|
395
|
+
return {
|
|
396
|
+
error: error?.message || "Tool execution failed",
|
|
397
|
+
content: fullContent || undefined,
|
|
398
|
+
finishReason: "tool_call",
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
content: fullContent || undefined,
|
|
406
|
+
finishReason,
|
|
407
|
+
};
|
|
408
|
+
} catch (error: any) {
|
|
409
|
+
return {
|
|
410
|
+
error: error?.message || "Streaming failed",
|
|
411
|
+
content: fullContent || undefined,
|
|
412
|
+
finishReason,
|
|
413
|
+
};
|
|
414
|
+
} finally {
|
|
415
|
+
reader.releaseLock();
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adminforth/completion-adapter-openai-responses",
|
|
3
|
+
"version": "2.0.21",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc && npm version patch",
|
|
9
|
+
"rollout": "npm run build && npm publish --access public"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": "",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"description": "AdminForth completion adapter for the OpenAI Responses API.",
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.9.3"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"openai": "^6.34.0",
|
|
20
|
+
"tiktoken": "^1.0.22"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"adminforth": "^2.24.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2016",
|
|
4
|
+
"module": "node16",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"strict": false,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"strictNullChecks": true
|
|
12
|
+
},
|
|
13
|
+
"exclude": ["node_modules", "dist", "custom"]
|
|
14
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface AdapterOptions {
|
|
2
|
+
/**
|
|
3
|
+
* OpenAI API key. Go to https://platform.openai.com/, go to Dashboard -> API keys -> Create new secret key
|
|
4
|
+
* Paste value in your .env file OPENAI_API_KEY=your_key
|
|
5
|
+
* Set openAiApiKey: process.env.OPENAI_API_KEY to access it
|
|
6
|
+
*/
|
|
7
|
+
openAiApiKey: string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Model name. Go to https://platform.openai.com/docs/models, select model and copy name.
|
|
11
|
+
* Default is `gpt-5-nano`.
|
|
12
|
+
*/
|
|
13
|
+
model?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Additional request body parameters to include in the API request.
|
|
17
|
+
*/
|
|
18
|
+
extraRequestBodyParameters?: Record<string, unknown>;
|
|
19
|
+
}
|