@adminforth/completion-adapter-openai-responses 2.0.25 → 2.0.27

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,9 @@
1
+ #!/bin/bash
2
+
3
+ pnpm run build 2>&1 | tee build.log
4
+ build_status=${PIPESTATUS[0]}
5
+
6
+ if [ $build_status -ne 0 ]; then
7
+ echo "Build failed. Exiting with status code $build_status"
8
+ exit $build_status
9
+ fi
@@ -0,0 +1,44 @@
1
+ #!/bin/sh
2
+
3
+ set -x
4
+
5
+ COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8)
6
+
7
+ STATUS=${1}
8
+
9
+ if [ "$STATUS" = "success" ]; then
10
+ MESSAGE="Did a build without issues on \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\`. Commit: _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
11
+
12
+ curl -s -X POST -H "Content-Type: application/json" -d '{
13
+ "username": "'"$CI_COMMIT_AUTHOR"'",
14
+ "icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
15
+ "attachments": [
16
+ {
17
+ "mrkdwn_in": ["text", "pretext"],
18
+ "color": "#36a64f",
19
+ "text": "'"$MESSAGE"'"
20
+ }
21
+ ]
22
+ }' "$DEVELOPERS_SLACK_WEBHOOK"
23
+ exit 0
24
+ fi
25
+ export BUILD_LOG=$(cat ./build.log)
26
+
27
+ BUILD_LOG=$(echo $BUILD_LOG | sed 's/"/\\"/g')
28
+
29
+ MESSAGE="Broke \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\` with commit _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
30
+ CODE_BLOCK="\`\`\`$BUILD_LOG\n\`\`\`"
31
+
32
+ echo "Sending slack message to developers $MESSAGE"
33
+ curl -sS -X POST -H "Content-Type: application/json" -d '{
34
+ "username": "'"$CI_COMMIT_AUTHOR"'",
35
+ "icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
36
+ "attachments": [
37
+ {
38
+ "mrkdwn_in": ["text", "pretext"],
39
+ "color": "#8A1C12",
40
+ "text": "'"$CODE_BLOCK"'",
41
+ "pretext": "'"$MESSAGE"'"
42
+ }
43
+ ]
44
+ }' "$DEVELOPERS_SLACK_WEBHOOK" 2>&1
@@ -0,0 +1,56 @@
1
+ clone:
2
+ git:
3
+ image: woodpeckerci/plugin-git
4
+ settings:
5
+ partial: false
6
+ depth: 5
7
+
8
+ steps:
9
+ init-secrets:
10
+ when:
11
+ - event: push
12
+ image: infisical/cli
13
+ environment:
14
+ INFISICAL_TOKEN:
15
+ from_secret: VAULT_TOKEN
16
+ commands:
17
+ - infisical export --domain https://vault.devforth.io/api --format=dotenv-export --env="prod" > /woodpecker/deploy.vault.env
18
+
19
+ build:
20
+ image: devforth/node20-pnpm:latest
21
+ when:
22
+ - event: push
23
+ commands:
24
+ - . /woodpecker/deploy.vault.env
25
+ - pnpm install
26
+ - /bin/bash ./.woodpecker/buildRelease.sh
27
+ - npm audit signatures
28
+
29
+ release:
30
+ image: devforth/node20-pnpm:latest
31
+ when:
32
+ - event:
33
+ - push
34
+ branch:
35
+ - main
36
+ commands:
37
+ - . /woodpecker/deploy.vault.env
38
+ - pnpm exec semantic-release
39
+
40
+ slack-on-failure:
41
+ image: curlimages/curl
42
+ when:
43
+ - event: push
44
+ status: [failure]
45
+ commands:
46
+ - . /woodpecker/deploy.vault.env
47
+ - /bin/sh ./.woodpecker/buildSlackNotify.sh failure
48
+
49
+ slack-on-success:
50
+ image: curlimages/curl
51
+ when:
52
+ - event: push
53
+ status: [success]
54
+ commands:
55
+ - . /woodpecker/deploy.vault.env
56
+ - /bin/sh ./.woodpecker/buildSlackNotify.sh success
package/dist/index.d.ts CHANGED
@@ -12,13 +12,28 @@ type CompletionRequestInput = {
12
12
  reasoningEffort?: ReasoningEffort;
13
13
  tools?: CompletionTool[];
14
14
  onChunk?: StreamChunkCallback;
15
+ signal?: AbortSignal;
16
+ };
17
+ type UsedTokens = {
18
+ input_uncached: number;
19
+ input_cached: number;
20
+ output: number;
21
+ };
22
+ type CompletionResult = {
23
+ content?: string;
24
+ finishReason?: string;
25
+ error?: string;
26
+ used_tokens?: UsedTokens;
15
27
  };
16
28
  export default class CompletionAdapterOpenAIResponses implements CompletionAdapter {
17
29
  options: AdapterOptions;
18
30
  private encoding;
31
+ private activeAbortController;
19
32
  constructor(options: AdapterOptions);
20
33
  validate(): void;
21
34
  measureTokensCount(content: string): number;
35
+ abort(): void;
36
+ isGenerationInProgress(): boolean;
22
37
  private getConfiguredBaseUrl;
23
38
  private shouldUseComplitionApi;
24
39
  private shouldDumpRawRequest;
@@ -35,9 +50,5 @@ export default class CompletionAdapterOpenAIResponses implements CompletionAdapt
35
50
  model: ChatOpenAI<import("@langchain/openai").ChatOpenAICallOptions>;
36
51
  middleware: import("langchain").AgentMiddleware<undefined, undefined, unknown, readonly (import("@langchain/core/tools").ClientTool | import("@langchain/core/tools").ServerTool)[]>[];
37
52
  };
38
- complete: (requestOrContent: CompletionRequestInput | string, maxTokens?: number, outputSchema?: any, reasoningEffort?: ReasoningEffort, toolsOrOnChunk?: CompletionTool[] | StreamChunkCallback, onChunk?: StreamChunkCallback) => Promise<{
39
- content?: string;
40
- finishReason?: string;
41
- error?: string;
42
- }>;
53
+ complete: (requestOrContent: CompletionRequestInput | string, maxTokens?: number, outputSchema?: any, reasoningEffort?: ReasoningEffort, toolsOrOnChunk?: CompletionTool[] | StreamChunkCallback, onChunk?: StreamChunkCallback) => Promise<CompletionResult>;
43
54
  }
package/dist/index.js CHANGED
@@ -68,6 +68,19 @@ function extractFunctionCall(data) {
68
68
  }
69
69
  return undefined;
70
70
  }
71
+ function extractUsedTokens(data) {
72
+ var _a, _b;
73
+ const usage = data.usage;
74
+ if (!usage) {
75
+ return undefined;
76
+ }
77
+ const inputCached = (_b = (_a = usage.input_tokens_details) === null || _a === void 0 ? void 0 : _a.cached_tokens) !== null && _b !== void 0 ? _b : 0;
78
+ return {
79
+ input_uncached: Math.max(usage.input_tokens - inputCached, 0),
80
+ input_cached: inputCached,
81
+ output: usage.output_tokens,
82
+ };
83
+ }
71
84
  function executeToolCall(toolCall, tools) {
72
85
  return __awaiter(this, void 0, void 0, function* () {
73
86
  const tool = tools === null || tools === void 0 ? void 0 : tools.find((candidate) => candidate.name === toolCall.name);
@@ -101,7 +114,7 @@ function getAgentReasoningEffort(purpose) {
101
114
  }
102
115
  function buildReasoningConfig(params) {
103
116
  var _a;
104
- return Object.assign({ summary: "detailed", effort: params.effort }, ((_a = params.reasoning) !== null && _a !== void 0 ? _a : {}));
117
+ return Object.assign({ summary: "auto", effort: params.effort }, ((_a = params.reasoning) !== null && _a !== void 0 ? _a : {}));
105
118
  }
106
119
  function getTurnKey(context) {
107
120
  return `${context.sessionId}:${context.turnId}`;
@@ -160,6 +173,7 @@ function createOpenAiResponsesContinuationMiddleware() {
160
173
  }
161
174
  export default class CompletionAdapterOpenAIResponses {
162
175
  constructor(options) {
176
+ this.activeAbortController = null;
163
177
  this.complete = (requestOrContent_1, ...args_1) => __awaiter(this, [requestOrContent_1, ...args_1], void 0, function* (requestOrContent, maxTokens = 50, outputSchema, reasoningEffort = "low", toolsOrOnChunk, onChunk) {
164
178
  var _a, _b, _c;
165
179
  const request = typeof requestOrContent === "string"
@@ -174,7 +188,7 @@ export default class CompletionAdapterOpenAIResponses {
174
188
  : onChunk,
175
189
  }
176
190
  : requestOrContent;
177
- const { content, maxTokens: requestMaxTokens = 50, outputSchema: requestOutputSchema, reasoningEffort: requestReasoningEffort = "low", tools, onChunk: streamChunkCallback, } = request;
191
+ const { content, maxTokens: requestMaxTokens = 50, outputSchema: requestOutputSchema, reasoningEffort: requestReasoningEffort = "low", tools, onChunk: streamChunkCallback, signal: requestSignal, } = request;
178
192
  const model = this.options.model || "gpt-5-nano";
179
193
  const isStreaming = typeof streamChunkCallback === "function";
180
194
  const extra = this.options.extraRequestBodyParameters;
@@ -205,212 +219,275 @@ export default class CompletionAdapterOpenAIResponses {
205
219
  if (this.shouldDumpRawRequest()) {
206
220
  this.dumpRawRequest(this.getResponsesUrl(), serializedBody);
207
221
  }
208
- const resp = yield fetch(this.getResponsesUrl(), {
209
- method: "POST",
210
- headers: {
211
- "Content-Type": "application/json",
212
- Authorization: `Bearer ${this.options.openAiApiKey}`,
213
- },
214
- body: serializedBody,
215
- });
216
- if (!resp.ok) {
217
- let errorMessage = `OpenAI request failed with status ${resp.status}`;
218
- try {
219
- const errorData = (yield resp.json());
220
- if ((_a = errorData.error) === null || _a === void 0 ? void 0 : _a.message)
221
- errorMessage = errorData.error.message;
222
- }
223
- catch (_e) { }
224
- return { error: errorMessage };
222
+ const abortController = new AbortController();
223
+ this.activeAbortController = abortController;
224
+ const abortFromRequestSignal = () => abortController.abort(requestSignal === null || requestSignal === void 0 ? void 0 : requestSignal.reason);
225
+ if (requestSignal === null || requestSignal === void 0 ? void 0 : requestSignal.aborted) {
226
+ abortController.abort(requestSignal.reason);
227
+ }
228
+ else {
229
+ requestSignal === null || requestSignal === void 0 ? void 0 : requestSignal.addEventListener("abort", abortFromRequestSignal, { once: true });
225
230
  }
226
- if (!isStreaming) {
227
- const json = yield resp.json();
228
- const data = json;
229
- if (data.error) {
230
- return { error: data.error.message };
231
+ let resp = null;
232
+ try {
233
+ resp = yield fetch(this.getResponsesUrl(), {
234
+ method: "POST",
235
+ headers: {
236
+ "Content-Type": "application/json",
237
+ Authorization: `Bearer ${this.options.openAiApiKey}`,
238
+ },
239
+ body: serializedBody,
240
+ signal: abortController.signal,
241
+ });
242
+ }
243
+ catch (error) {
244
+ if (this.activeAbortController === abortController) {
245
+ this.activeAbortController = null;
231
246
  }
232
- const toolCall = extractFunctionCall(data);
233
- if (toolCall) {
234
- try {
235
- const toolResult = yield executeToolCall(toolCall, tools);
236
- return {
237
- content: toolResult,
238
- finishReason: "tool_call",
239
- };
240
- }
241
- catch (error) {
242
- return {
243
- error: (error === null || error === void 0 ? void 0 : error.message) || "Tool execution failed",
244
- finishReason: "tool_call",
245
- };
246
- }
247
+ if (abortController.signal.aborted) {
248
+ return {
249
+ error: (error === null || error === void 0 ? void 0 : error.message) || "Generation aborted",
250
+ finishReason: "aborted",
251
+ };
247
252
  }
248
- const parsedContent = extractOutputText(data);
249
253
  return {
250
- content: parsedContent,
251
- finishReason: ((_b = data.incomplete_details) === null || _b === void 0 ? void 0 : _b.reason)
252
- ? data.incomplete_details.reason
253
- : undefined,
254
+ error: (error === null || error === void 0 ? void 0 : error.message) || "OpenAI request failed",
254
255
  };
255
256
  }
256
- if (!resp.body) {
257
- return { error: "Response body is empty" };
257
+ finally {
258
+ requestSignal === null || requestSignal === void 0 ? void 0 : requestSignal.removeEventListener("abort", abortFromRequestSignal);
258
259
  }
259
- const reader = resp.body.getReader();
260
- const decoder = new TextDecoder("utf-8");
261
- let buffer = "";
262
- let fullContent = "";
263
- let fullReasoning = "";
264
- let finishReason;
265
- let completedResponse;
266
- const handleEvent = (event, eventType) => __awaiter(this, void 0, void 0, function* () {
267
- var _a, _b, _c, _d;
268
- const type = (event === null || event === void 0 ? void 0 : event.type) || eventType;
269
- if (type === "response.output_text.delta") {
270
- const delta = (event === null || event === void 0 ? void 0 : event.delta) || "";
271
- if (!delta)
272
- return;
273
- fullContent += delta;
274
- yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, { type: "output", delta, text: fullContent }));
275
- return;
276
- }
277
- if (type === "response.reasoning_summary_text.delta" ||
278
- type === "response.reasoning_text.delta") {
279
- const delta = (event === null || event === void 0 ? void 0 : event.delta) || "";
280
- if (!delta)
281
- return;
282
- fullReasoning += delta;
283
- yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
284
- type: "reasoning",
285
- delta,
286
- text: fullReasoning,
287
- }));
288
- return;
289
- }
290
- if (type === "response.completed" || type === "response.incomplete") {
291
- const response = event === null || event === void 0 ? void 0 : event.response;
292
- if (!response)
293
- return;
294
- const finalContent = extractOutputText(response);
295
- if (finalContent.startsWith(fullContent)) {
296
- const delta = finalContent.slice(fullContent.length);
297
- if (delta) {
298
- fullContent = finalContent;
299
- yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
300
- type: "output",
301
- delta,
302
- text: fullContent,
303
- }));
304
- }
305
- }
306
- const finalReasoning = extractReasoning(response) || "";
307
- if (finalReasoning.startsWith(fullReasoning)) {
308
- const delta = finalReasoning.slice(fullReasoning.length);
309
- if (delta) {
310
- fullReasoning = finalReasoning;
311
- yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
312
- type: "reasoning",
313
- delta,
314
- text: fullReasoning,
315
- }));
316
- }
317
- }
318
- finishReason =
319
- ((_a = response.incomplete_details) === null || _a === void 0 ? void 0 : _a.reason) || response.status || finishReason;
320
- completedResponse = response;
321
- return;
322
- }
323
- if (type === "response.failed") {
324
- 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) ||
325
- ((_d = event === null || event === void 0 ? void 0 : event.error) === null || _d === void 0 ? void 0 : _d.message) ||
326
- "Response failed");
260
+ if (!resp) {
261
+ if (this.activeAbortController === abortController) {
262
+ this.activeAbortController = null;
327
263
  }
328
- });
264
+ return {
265
+ error: "OpenAI request failed",
266
+ };
267
+ }
329
268
  try {
330
- while (true) {
331
- const { value, done } = yield reader.read();
332
- if (done)
333
- break;
334
- buffer += decoder.decode(value, { stream: true });
335
- const blocks = buffer.split("\n\n");
336
- buffer = blocks.pop() || "";
337
- for (const block of blocks) {
338
- const parsedBlock = parseSseBlock(block);
339
- if (!(parsedBlock === null || parsedBlock === void 0 ? void 0 : parsedBlock.data) || parsedBlock.data === "[DONE]")
340
- continue;
341
- let event;
342
- try {
343
- event = JSON.parse(parsedBlock.data);
344
- }
345
- catch (_f) {
346
- continue;
347
- }
348
- if ((_c = event === null || event === void 0 ? void 0 : event.error) === null || _c === void 0 ? void 0 : _c.message) {
349
- return { error: event.error.message };
350
- }
351
- yield handleEvent(event, parsedBlock.event);
269
+ if (!resp.ok) {
270
+ let errorMessage = `OpenAI request failed with status ${resp.status}`;
271
+ try {
272
+ const errorData = (yield resp.json());
273
+ if ((_a = errorData.error) === null || _a === void 0 ? void 0 : _a.message)
274
+ errorMessage = errorData.error.message;
352
275
  }
276
+ catch (_e) { }
277
+ return { error: errorMessage };
353
278
  }
354
- if (buffer.trim()) {
355
- const parsedBlock = parseSseBlock(buffer.trim());
356
- if ((parsedBlock === null || parsedBlock === void 0 ? void 0 : parsedBlock.data) && parsedBlock.data !== "[DONE]") {
357
- try {
358
- yield handleEvent(JSON.parse(parsedBlock.data), parsedBlock.event);
359
- }
360
- catch (error) {
361
- return {
362
- error: (error === null || error === void 0 ? void 0 : error.message) || "Streaming failed",
363
- content: fullContent || undefined,
364
- finishReason,
365
- };
366
- }
279
+ if (!isStreaming) {
280
+ const json = yield resp.json();
281
+ const data = json;
282
+ if (data.error) {
283
+ return { error: data.error.message };
367
284
  }
368
- }
369
- if (completedResponse) {
370
- const toolCall = extractFunctionCall(completedResponse);
285
+ const usedTokens = extractUsedTokens(data);
286
+ const toolCall = extractFunctionCall(data);
371
287
  if (toolCall) {
372
288
  try {
373
289
  const toolResult = yield executeToolCall(toolCall, tools);
374
- if (toolResult) {
375
- const delta = toolResult.startsWith(fullContent)
376
- ? toolResult.slice(fullContent.length)
377
- : toolResult;
378
- if (delta) {
379
- yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
380
- type: "output",
381
- delta,
382
- text: toolResult,
383
- }));
384
- }
385
- }
386
290
  return {
387
291
  content: toolResult,
388
292
  finishReason: "tool_call",
293
+ used_tokens: usedTokens,
389
294
  };
390
295
  }
391
296
  catch (error) {
392
297
  return {
393
298
  error: (error === null || error === void 0 ? void 0 : error.message) || "Tool execution failed",
394
- content: fullContent || undefined,
395
299
  finishReason: "tool_call",
300
+ used_tokens: usedTokens,
396
301
  };
397
302
  }
398
303
  }
304
+ const parsedContent = extractOutputText(data);
305
+ return {
306
+ content: parsedContent,
307
+ finishReason: ((_b = data.incomplete_details) === null || _b === void 0 ? void 0 : _b.reason)
308
+ ? data.incomplete_details.reason
309
+ : undefined,
310
+ used_tokens: usedTokens,
311
+ };
312
+ }
313
+ if (!resp.body) {
314
+ return { error: "Response body is empty" };
315
+ }
316
+ const reader = resp.body.getReader();
317
+ const decoder = new TextDecoder("utf-8");
318
+ let buffer = "";
319
+ let fullContent = "";
320
+ let fullReasoning = "";
321
+ let finishReason;
322
+ let completedResponse;
323
+ let usedTokens;
324
+ const handleEvent = (event, eventType) => __awaiter(this, void 0, void 0, function* () {
325
+ var _a, _b, _c, _d;
326
+ const type = (event === null || event === void 0 ? void 0 : event.type) || eventType;
327
+ if (type === "response.output_text.delta") {
328
+ const delta = (event === null || event === void 0 ? void 0 : event.delta) || "";
329
+ if (!delta)
330
+ return;
331
+ fullContent += delta;
332
+ yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, { type: "output", delta, text: fullContent }));
333
+ return;
334
+ }
335
+ if (type === "response.reasoning_summary_text.delta" ||
336
+ type === "response.reasoning_text.delta") {
337
+ const delta = (event === null || event === void 0 ? void 0 : event.delta) || "";
338
+ if (!delta)
339
+ return;
340
+ fullReasoning += delta;
341
+ yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
342
+ type: "reasoning",
343
+ delta,
344
+ text: fullReasoning,
345
+ }));
346
+ return;
347
+ }
348
+ if (type === "response.completed" || type === "response.incomplete") {
349
+ const response = event === null || event === void 0 ? void 0 : event.response;
350
+ if (!response)
351
+ return;
352
+ const finalContent = extractOutputText(response);
353
+ if (finalContent.startsWith(fullContent)) {
354
+ const delta = finalContent.slice(fullContent.length);
355
+ if (delta) {
356
+ fullContent = finalContent;
357
+ yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
358
+ type: "output",
359
+ delta,
360
+ text: fullContent,
361
+ }));
362
+ }
363
+ }
364
+ const finalReasoning = extractReasoning(response) || "";
365
+ if (finalReasoning.startsWith(fullReasoning)) {
366
+ const delta = finalReasoning.slice(fullReasoning.length);
367
+ if (delta) {
368
+ fullReasoning = finalReasoning;
369
+ yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
370
+ type: "reasoning",
371
+ delta,
372
+ text: fullReasoning,
373
+ }));
374
+ }
375
+ }
376
+ finishReason =
377
+ ((_a = response.incomplete_details) === null || _a === void 0 ? void 0 : _a.reason) || response.status || finishReason;
378
+ completedResponse = response;
379
+ usedTokens = extractUsedTokens(response);
380
+ return;
381
+ }
382
+ if (type === "response.failed") {
383
+ 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) ||
384
+ ((_d = event === null || event === void 0 ? void 0 : event.error) === null || _d === void 0 ? void 0 : _d.message) ||
385
+ "Response failed");
386
+ }
387
+ });
388
+ try {
389
+ while (true) {
390
+ const { value, done } = yield reader.read();
391
+ if (done)
392
+ break;
393
+ buffer += decoder.decode(value, { stream: true });
394
+ const blocks = buffer.split("\n\n");
395
+ buffer = blocks.pop() || "";
396
+ for (const block of blocks) {
397
+ const parsedBlock = parseSseBlock(block);
398
+ if (!(parsedBlock === null || parsedBlock === void 0 ? void 0 : parsedBlock.data) || parsedBlock.data === "[DONE]")
399
+ continue;
400
+ let event;
401
+ try {
402
+ event = JSON.parse(parsedBlock.data);
403
+ }
404
+ catch (_f) {
405
+ continue;
406
+ }
407
+ if ((_c = event === null || event === void 0 ? void 0 : event.error) === null || _c === void 0 ? void 0 : _c.message) {
408
+ return { error: event.error.message };
409
+ }
410
+ yield handleEvent(event, parsedBlock.event);
411
+ }
412
+ }
413
+ if (buffer.trim()) {
414
+ const parsedBlock = parseSseBlock(buffer.trim());
415
+ if ((parsedBlock === null || parsedBlock === void 0 ? void 0 : parsedBlock.data) && parsedBlock.data !== "[DONE]") {
416
+ try {
417
+ yield handleEvent(JSON.parse(parsedBlock.data), parsedBlock.event);
418
+ }
419
+ catch (error) {
420
+ return {
421
+ error: (error === null || error === void 0 ? void 0 : error.message) || "Streaming failed",
422
+ content: fullContent || undefined,
423
+ finishReason,
424
+ };
425
+ }
426
+ }
427
+ }
428
+ if (completedResponse) {
429
+ const toolCall = extractFunctionCall(completedResponse);
430
+ if (toolCall) {
431
+ try {
432
+ const toolResult = yield executeToolCall(toolCall, tools);
433
+ if (toolResult) {
434
+ const delta = toolResult.startsWith(fullContent)
435
+ ? toolResult.slice(fullContent.length)
436
+ : toolResult;
437
+ if (delta) {
438
+ yield (streamChunkCallback === null || streamChunkCallback === void 0 ? void 0 : streamChunkCallback(delta, {
439
+ type: "output",
440
+ delta,
441
+ text: toolResult,
442
+ }));
443
+ }
444
+ }
445
+ return {
446
+ content: toolResult,
447
+ finishReason: "tool_call",
448
+ used_tokens: usedTokens,
449
+ };
450
+ }
451
+ catch (error) {
452
+ return {
453
+ error: (error === null || error === void 0 ? void 0 : error.message) || "Tool execution failed",
454
+ content: fullContent || undefined,
455
+ finishReason: "tool_call",
456
+ used_tokens: usedTokens,
457
+ };
458
+ }
459
+ }
460
+ }
461
+ return {
462
+ content: fullContent || undefined,
463
+ finishReason,
464
+ used_tokens: usedTokens,
465
+ };
466
+ }
467
+ catch (error) {
468
+ if (abortController.signal.aborted) {
469
+ return {
470
+ error: (error === null || error === void 0 ? void 0 : error.message) || "Generation aborted",
471
+ content: fullContent || undefined,
472
+ finishReason: "aborted",
473
+ used_tokens: usedTokens,
474
+ };
475
+ }
476
+ return {
477
+ error: (error === null || error === void 0 ? void 0 : error.message) || "Streaming failed",
478
+ content: fullContent || undefined,
479
+ finishReason,
480
+ used_tokens: usedTokens,
481
+ };
482
+ }
483
+ finally {
484
+ reader.releaseLock();
399
485
  }
400
- return {
401
- content: fullContent || undefined,
402
- finishReason,
403
- };
404
- }
405
- catch (error) {
406
- return {
407
- error: (error === null || error === void 0 ? void 0 : error.message) || "Streaming failed",
408
- content: fullContent || undefined,
409
- finishReason,
410
- };
411
486
  }
412
487
  finally {
413
- reader.releaseLock();
488
+ if (this.activeAbortController === abortController) {
489
+ this.activeAbortController = null;
490
+ }
414
491
  }
415
492
  });
416
493
  this.options = options;
@@ -432,6 +509,13 @@ export default class CompletionAdapterOpenAIResponses {
432
509
  measureTokensCount(content) {
433
510
  return this.encoding.encode(content).length;
434
511
  }
512
+ abort() {
513
+ var _a;
514
+ (_a = this.activeAbortController) === null || _a === void 0 ? void 0 : _a.abort();
515
+ }
516
+ isGenerationInProgress() {
517
+ return Boolean(this.activeAbortController);
518
+ }
435
519
  getConfiguredBaseUrl() {
436
520
  return this.options.baseUrl;
437
521
  }
package/index.ts CHANGED
@@ -34,6 +34,7 @@ type CompletionRequestInput = {
34
34
  reasoningEffort?: ReasoningEffort;
35
35
  tools?: CompletionTool[];
36
36
  onChunk?: StreamChunkCallback;
37
+ signal?: AbortSignal;
37
38
  };
38
39
 
39
40
  type ResponseCreateBody = OpenAI.Responses.ResponseCreateParams;
@@ -59,6 +60,20 @@ type OpenAiResponsesMetadata = {
59
60
  type OpenAiResponsesContext = {
60
61
  sessionId: string;
61
62
  turnId: string;
63
+ abortSignal?: AbortSignal;
64
+ };
65
+
66
+ type UsedTokens = {
67
+ input_uncached: number;
68
+ input_cached: number;
69
+ output: number;
70
+ };
71
+
72
+ type CompletionResult = {
73
+ content?: string;
74
+ finishReason?: string;
75
+ error?: string;
76
+ used_tokens?: UsedTokens;
62
77
  };
63
78
 
64
79
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
@@ -118,6 +133,21 @@ function extractFunctionCall(
118
133
  return undefined;
119
134
  }
120
135
 
136
+ function extractUsedTokens(data: OpenAIResponsesSuccess): UsedTokens | undefined {
137
+ const usage = data.usage;
138
+ if (!usage) {
139
+ return undefined;
140
+ }
141
+
142
+ const inputCached = usage.input_tokens_details?.cached_tokens ?? 0;
143
+
144
+ return {
145
+ input_uncached: Math.max(usage.input_tokens - inputCached, 0),
146
+ input_cached: inputCached,
147
+ output: usage.output_tokens,
148
+ };
149
+ }
150
+
121
151
  async function executeToolCall(
122
152
  toolCall: OpenAIFunctionCall,
123
153
  tools?: CompletionTool[],
@@ -158,7 +188,7 @@ function buildReasoningConfig(params: {
158
188
  effort: Exclude<ReasoningEffort, "none"> | ReasoningEffort;
159
189
  }) {
160
190
  return {
161
- summary: "detailed",
191
+ summary: "auto",
162
192
  effort: params.effort,
163
193
  ...(params.reasoning ?? {}),
164
194
  };
@@ -250,6 +280,7 @@ export default class CompletionAdapterOpenAIResponses
250
280
  {
251
281
  options: AdapterOptions;
252
282
  private encoding: ReturnType<typeof encoding_for_model>;
283
+ private activeAbortController: AbortController | null = null;
253
284
 
254
285
  constructor(options: AdapterOptions) {
255
286
  this.options = options;
@@ -275,6 +306,14 @@ export default class CompletionAdapterOpenAIResponses
275
306
  return this.encoding.encode(content).length;
276
307
  }
277
308
 
309
+ abort() {
310
+ this.activeAbortController?.abort();
311
+ }
312
+
313
+ isGenerationInProgress() {
314
+ return Boolean(this.activeAbortController);
315
+ }
316
+
278
317
  private getConfiguredBaseUrl() {
279
318
  return this.options.baseUrl;
280
319
  }
@@ -409,11 +448,7 @@ export default class CompletionAdapterOpenAIResponses
409
448
  reasoningEffort: ReasoningEffort = "low",
410
449
  toolsOrOnChunk?: CompletionTool[] | StreamChunkCallback,
411
450
  onChunk?: StreamChunkCallback,
412
- ): Promise<{
413
- content?: string;
414
- finishReason?: string;
415
- error?: string;
416
- }> => {
451
+ ): Promise<CompletionResult> => {
417
452
  const request =
418
453
  typeof requestOrContent === "string"
419
454
  ? {
@@ -435,6 +470,7 @@ export default class CompletionAdapterOpenAIResponses
435
470
  reasoningEffort: requestReasoningEffort = "low",
436
471
  tools,
437
472
  onChunk: streamChunkCallback,
473
+ signal: requestSignal,
438
474
  } = request;
439
475
  const model = this.options.model || "gpt-5-nano";
440
476
  const isStreaming = typeof streamChunkCallback === "function";
@@ -487,69 +523,117 @@ export default class CompletionAdapterOpenAIResponses
487
523
  this.dumpRawRequest(this.getResponsesUrl(), serializedBody);
488
524
  }
489
525
 
490
- const resp = await fetch(this.getResponsesUrl(), {
491
- method: "POST",
492
- headers: {
493
- "Content-Type": "application/json",
494
- Authorization: `Bearer ${this.options.openAiApiKey}`,
495
- },
496
- body: serializedBody,
497
- });
498
-
499
- if (!resp.ok) {
500
- let errorMessage = `OpenAI request failed with status ${resp.status}`;
501
- try {
502
- const errorData = (await resp.json()) as OpenAIErrorResponse;
503
- if (errorData.error?.message) errorMessage = errorData.error.message;
504
- } catch {}
505
- return { error: errorMessage };
526
+ const abortController = new AbortController();
527
+ this.activeAbortController = abortController;
528
+ const abortFromRequestSignal = () => abortController.abort(requestSignal?.reason);
529
+
530
+ if (requestSignal?.aborted) {
531
+ abortController.abort(requestSignal.reason);
532
+ } else {
533
+ requestSignal?.addEventListener("abort", abortFromRequestSignal, { once: true });
506
534
  }
507
535
 
508
- if (!isStreaming) {
509
- const json = await resp.json();
510
- const data = json as OpenAIResponsesSuccess & OpenAIErrorResponse;
511
- if (data.error) {
512
- return { error: data.error.message };
536
+ let resp: Response | null = null;
537
+ try {
538
+ resp = await fetch(this.getResponsesUrl(), {
539
+ method: "POST",
540
+ headers: {
541
+ "Content-Type": "application/json",
542
+ Authorization: `Bearer ${this.options.openAiApiKey}`,
543
+ },
544
+ body: serializedBody,
545
+ signal: abortController.signal,
546
+ });
547
+ } catch (error: any) {
548
+ if (this.activeAbortController === abortController) {
549
+ this.activeAbortController = null;
513
550
  }
514
551
 
515
- const toolCall = extractFunctionCall(data);
516
- if (toolCall) {
517
- try {
518
- const toolResult = await executeToolCall(toolCall, tools);
519
- return {
520
- content: toolResult,
521
- finishReason: "tool_call",
522
- };
523
- } catch (error: any) {
524
- return {
525
- error: error?.message || "Tool execution failed",
526
- finishReason: "tool_call",
527
- };
528
- }
552
+ if (abortController.signal.aborted) {
553
+ return {
554
+ error: error?.message || "Generation aborted",
555
+ finishReason: "aborted",
556
+ };
529
557
  }
530
558
 
531
- const parsedContent = extractOutputText(data);
532
-
533
559
  return {
534
- content: parsedContent,
535
- finishReason: data.incomplete_details?.reason
536
- ? data.incomplete_details.reason
537
- : undefined,
560
+ error: error?.message || "OpenAI request failed",
538
561
  };
562
+ } finally {
563
+ requestSignal?.removeEventListener("abort", abortFromRequestSignal);
539
564
  }
540
565
 
541
- if (!resp.body) {
542
- return { error: "Response body is empty" };
566
+ if (!resp) {
567
+ if (this.activeAbortController === abortController) {
568
+ this.activeAbortController = null;
569
+ }
570
+
571
+ return {
572
+ error: "OpenAI request failed",
573
+ };
543
574
  }
544
575
 
545
- const reader = resp.body.getReader();
546
- const decoder = new TextDecoder("utf-8");
576
+ try {
577
+ if (!resp.ok) {
578
+ let errorMessage = `OpenAI request failed with status ${resp.status}`;
579
+ try {
580
+ const errorData = (await resp.json()) as OpenAIErrorResponse;
581
+ if (errorData.error?.message) errorMessage = errorData.error.message;
582
+ } catch {}
583
+ return { error: errorMessage };
584
+ }
547
585
 
548
- let buffer = "";
549
- let fullContent = "";
550
- let fullReasoning = "";
551
- let finishReason: string | undefined;
552
- let completedResponse: OpenAIResponsesSuccess | undefined;
586
+ if (!isStreaming) {
587
+ const json = await resp.json();
588
+ const data = json as OpenAIResponsesSuccess & OpenAIErrorResponse;
589
+ if (data.error) {
590
+ return { error: data.error.message };
591
+ }
592
+
593
+ const usedTokens = extractUsedTokens(data);
594
+
595
+ const toolCall = extractFunctionCall(data);
596
+ if (toolCall) {
597
+ try {
598
+ const toolResult = await executeToolCall(toolCall, tools);
599
+ return {
600
+ content: toolResult,
601
+ finishReason: "tool_call",
602
+ used_tokens: usedTokens,
603
+ };
604
+ } catch (error: any) {
605
+ return {
606
+ error: error?.message || "Tool execution failed",
607
+ finishReason: "tool_call",
608
+ used_tokens: usedTokens,
609
+ };
610
+ }
611
+ }
612
+
613
+ const parsedContent = extractOutputText(data);
614
+
615
+ return {
616
+ content: parsedContent,
617
+ finishReason: data.incomplete_details?.reason
618
+ ? data.incomplete_details.reason
619
+ : undefined,
620
+ used_tokens: usedTokens,
621
+ };
622
+ }
623
+
624
+ if (!resp.body) {
625
+ return { error: "Response body is empty" };
626
+ }
627
+
628
+ const reader = resp.body.getReader();
629
+ const decoder = new TextDecoder("utf-8");
630
+
631
+ let buffer = "";
632
+ let fullContent = "";
633
+ let fullReasoning = "";
634
+ let finishReason: string | undefined;
635
+ let completedResponse: OpenAIResponsesSuccess | undefined;
636
+ let usedTokens: UsedTokens | undefined;
553
637
 
554
638
  const handleEvent = async (event: any, eventType?: string) => {
555
639
  const type = event?.type || eventType;
@@ -610,6 +694,7 @@ export default class CompletionAdapterOpenAIResponses
610
694
  finishReason =
611
695
  response.incomplete_details?.reason || response.status || finishReason;
612
696
  completedResponse = response;
697
+ usedTokens = extractUsedTokens(response);
613
698
  return;
614
699
  }
615
700
 
@@ -687,12 +772,14 @@ export default class CompletionAdapterOpenAIResponses
687
772
  return {
688
773
  content: toolResult,
689
774
  finishReason: "tool_call",
775
+ used_tokens: usedTokens,
690
776
  };
691
777
  } catch (error: any) {
692
778
  return {
693
779
  error: error?.message || "Tool execution failed",
694
780
  content: fullContent || undefined,
695
781
  finishReason: "tool_call",
782
+ used_tokens: usedTokens,
696
783
  };
697
784
  }
698
785
  }
@@ -701,15 +788,31 @@ export default class CompletionAdapterOpenAIResponses
701
788
  return {
702
789
  content: fullContent || undefined,
703
790
  finishReason,
791
+ used_tokens: usedTokens,
704
792
  };
705
793
  } catch (error: any) {
794
+ if (abortController.signal.aborted) {
795
+ return {
796
+ error: error?.message || "Generation aborted",
797
+ content: fullContent || undefined,
798
+ finishReason: "aborted",
799
+ used_tokens: usedTokens,
800
+ };
801
+ }
802
+
706
803
  return {
707
804
  error: error?.message || "Streaming failed",
708
805
  content: fullContent || undefined,
709
806
  finishReason,
807
+ used_tokens: usedTokens,
710
808
  };
711
809
  } finally {
712
810
  reader.releaseLock();
713
811
  }
812
+ } finally {
813
+ if (this.activeAbortController === abortController) {
814
+ this.activeAbortController = null;
815
+ }
816
+ }
714
817
  };
715
818
  }
package/package.json CHANGED
@@ -1,18 +1,27 @@
1
1
  {
2
2
  "name": "@adminforth/completion-adapter-openai-responses",
3
- "version": "2.0.25",
3
+ "version": "2.0.27",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
7
+ "homepage": "https://adminforth.dev/docs/tutorial/Adapters/completion-adapter-openai-responses/",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/devforth/adminforth-completion-adapter-openai-responses.git"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
7
15
  "scripts": {
8
- "build": "tsc && npm version patch",
9
- "rollout": "npm run build && npm publish --access public"
16
+ "build": "tsc"
10
17
  },
11
18
  "keywords": [],
12
19
  "author": "DevForth (https://devforth.io)",
13
20
  "license": "MIT",
14
21
  "description": "AdminForth completion adapter for the OpenAI Responses API.",
15
22
  "devDependencies": {
23
+ "semantic-release": "^24.2.1",
24
+ "semantic-release-slack-bot": "^4.0.2",
16
25
  "typescript": "^5.9.3"
17
26
  },
18
27
  "dependencies": {
@@ -24,5 +33,30 @@
24
33
  },
25
34
  "peerDependencies": {
26
35
  "adminforth": "^2.24.0"
36
+ },
37
+ "release": {
38
+ "plugins": [
39
+ "@semantic-release/commit-analyzer",
40
+ "@semantic-release/release-notes-generator",
41
+ "@semantic-release/npm",
42
+ "@semantic-release/github",
43
+ [
44
+ "semantic-release-slack-bot",
45
+ {
46
+ "packageName": "@adminforth/completion-adapter-openai-responses",
47
+ "notifyOnSuccess": true,
48
+ "notifyOnFail": true,
49
+ "slackIcon": ":package:",
50
+ "markdownReleaseNotes": true
51
+ }
52
+ ]
53
+ ],
54
+ "branches": [
55
+ "main",
56
+ {
57
+ "name": "next",
58
+ "prerelease": true
59
+ }
60
+ ]
27
61
  }
28
62
  }