@adminforth/completion-adapter-openai-responses 2.0.24 → 2.0.26

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/README.md CHANGED
@@ -24,6 +24,19 @@ const adapter = new CompletionAdapterOpenAIResponses({
24
24
  });
25
25
  ```
26
26
 
27
+ OpenAI-compatible providers can be used by overriding the base URL:
28
+
29
+ ```ts
30
+ const adapter = new CompletionAdapterOpenAIResponses({
31
+ openAiApiKey: process.env.OVH_AI_ENDPOINTS_ACCESS_TOKEN as string,
32
+ baseUrl: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1",
33
+ model: "gpt-oss-20b",
34
+ extraRequestBodyParameters: {
35
+ store: false,
36
+ },
37
+ });
38
+ ```
39
+
27
40
  The adapter supports:
28
41
 
29
42
  - regular text completion
package/dist/index.d.ts CHANGED
@@ -19,6 +19,15 @@ export default class CompletionAdapterOpenAIResponses implements CompletionAdapt
19
19
  constructor(options: AdapterOptions);
20
20
  validate(): void;
21
21
  measureTokensCount(content: string): number;
22
+ private getConfiguredBaseUrl;
23
+ private shouldUseComplitionApi;
24
+ private shouldDumpRawRequest;
25
+ private getClientConfiguration;
26
+ private createResponsesDebugFetch;
27
+ private getFetchUrl;
28
+ private isResponsesUrl;
29
+ private dumpRawRequest;
30
+ private getResponsesUrl;
22
31
  getLangChainAgentSpec(params: {
23
32
  maxTokens: number;
24
33
  purpose: AgentModelPurpose;
package/dist/index.js CHANGED
@@ -22,6 +22,8 @@ import { AIMessage } from "@langchain/core/messages";
22
22
  import { ChatOpenAI } from "@langchain/openai";
23
23
  import { createMiddleware } from "langchain";
24
24
  import { encoding_for_model } from "tiktoken";
25
+ const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
26
+ const RAW_REQUEST_LOG_PREFIX = "[CompletionAdapterOpenAIResponses] Raw /responses request";
25
27
  function extractOutputText(data) {
26
28
  var _a;
27
29
  let text = "";
@@ -97,6 +99,10 @@ function parseSseBlock(block) {
97
99
  function getAgentReasoningEffort(purpose) {
98
100
  return purpose === "summary" ? "minimal" : "low";
99
101
  }
102
+ function buildReasoningConfig(params) {
103
+ var _a;
104
+ return Object.assign({ summary: "detailed", effort: params.effort }, ((_a = params.reasoning) !== null && _a !== void 0 ? _a : {}));
105
+ }
100
106
  function getTurnKey(context) {
101
107
  return `${context.sessionId}:${context.turnId}`;
102
108
  }
@@ -172,6 +178,7 @@ export default class CompletionAdapterOpenAIResponses {
172
178
  const model = this.options.model || "gpt-5-nano";
173
179
  const isStreaming = typeof streamChunkCallback === "function";
174
180
  const extra = this.options.extraRequestBodyParameters;
181
+ const _d = extra !== null && extra !== void 0 ? extra : {}, { reasoning: extraReasoning } = _d, extraWithoutReasoning = __rest(_d, ["reasoning"]);
175
182
  let openAiTools = undefined;
176
183
  if (tools && tools.length > 0) {
177
184
  openAiTools = tools.map((tool) => ({
@@ -179,7 +186,7 @@ export default class CompletionAdapterOpenAIResponses {
179
186
  name: tool.name,
180
187
  description: tool.description,
181
188
  parameters: tool.input_schema,
182
- strict: true,
189
+ strict: false,
183
190
  }));
184
191
  }
185
192
  const body = Object.assign({ model, input: content, max_output_tokens: requestMaxTokens, stream: isStreaming, text: requestOutputSchema
@@ -190,17 +197,21 @@ export default class CompletionAdapterOpenAIResponses {
190
197
  format: {
191
198
  type: "text",
192
199
  },
193
- }, reasoning: {
200
+ }, reasoning: Object.assign({}, buildReasoningConfig({
201
+ reasoning: extraReasoning,
194
202
  effort: requestReasoningEffort,
195
- summary: "auto",
196
- }, tools: openAiTools }, extra);
197
- const resp = yield fetch("https://api.openai.com/v1/responses", {
203
+ })), tools: openAiTools }, extraWithoutReasoning);
204
+ const serializedBody = JSON.stringify(body);
205
+ if (this.shouldDumpRawRequest()) {
206
+ this.dumpRawRequest(this.getResponsesUrl(), serializedBody);
207
+ }
208
+ const resp = yield fetch(this.getResponsesUrl(), {
198
209
  method: "POST",
199
210
  headers: {
200
211
  "Content-Type": "application/json",
201
212
  Authorization: `Bearer ${this.options.openAiApiKey}`,
202
213
  },
203
- body: JSON.stringify(body),
214
+ body: serializedBody,
204
215
  });
205
216
  if (!resp.ok) {
206
217
  let errorMessage = `OpenAI request failed with status ${resp.status}`;
@@ -209,7 +220,7 @@ export default class CompletionAdapterOpenAIResponses {
209
220
  if ((_a = errorData.error) === null || _a === void 0 ? void 0 : _a.message)
210
221
  errorMessage = errorData.error.message;
211
222
  }
212
- catch (_d) { }
223
+ catch (_e) { }
213
224
  return { error: errorMessage };
214
225
  }
215
226
  if (!isStreaming) {
@@ -331,7 +342,7 @@ export default class CompletionAdapterOpenAIResponses {
331
342
  try {
332
343
  event = JSON.parse(parsedBlock.data);
333
344
  }
334
- catch (_e) {
345
+ catch (_f) {
335
346
  continue;
336
347
  }
337
348
  if ((_c = event === null || event === void 0 ? void 0 : event.error) === null || _c === void 0 ? void 0 : _c.message) {
@@ -421,22 +432,96 @@ export default class CompletionAdapterOpenAIResponses {
421
432
  measureTokensCount(content) {
422
433
  return this.encoding.encode(content).length;
423
434
  }
435
+ getConfiguredBaseUrl() {
436
+ return this.options.baseUrl;
437
+ }
438
+ shouldUseComplitionApi() {
439
+ if (typeof this.options.useComplitionApi === "boolean") {
440
+ return this.options.useComplitionApi;
441
+ }
442
+ return Boolean(this.getConfiguredBaseUrl());
443
+ }
444
+ shouldDumpRawRequest() {
445
+ return this.options.dumpRawRequest === true;
446
+ }
447
+ getClientConfiguration() {
448
+ const configuredBaseUrl = this.getConfiguredBaseUrl();
449
+ const debugFetch = this.shouldDumpRawRequest()
450
+ ? this.createResponsesDebugFetch()
451
+ : undefined;
452
+ if (!configuredBaseUrl && !debugFetch) {
453
+ return undefined;
454
+ }
455
+ return Object.assign(Object.assign({}, (configuredBaseUrl ? { baseURL: configuredBaseUrl } : {})), (debugFetch ? { fetch: debugFetch } : {}));
456
+ }
457
+ createResponsesDebugFetch() {
458
+ return (input, init) => __awaiter(this, void 0, void 0, function* () {
459
+ const url = this.getFetchUrl(input);
460
+ if (this.isResponsesUrl(url) && typeof (init === null || init === void 0 ? void 0 : init.body) === "string") {
461
+ this.dumpRawRequest(url, init.body);
462
+ }
463
+ return fetch(input, init);
464
+ });
465
+ }
466
+ getFetchUrl(input) {
467
+ if (typeof input === "string") {
468
+ return input;
469
+ }
470
+ if (input instanceof URL) {
471
+ return input.toString();
472
+ }
473
+ return input.url;
474
+ }
475
+ isResponsesUrl(url) {
476
+ try {
477
+ return new URL(url).pathname.endsWith("/responses");
478
+ }
479
+ catch (_a) {
480
+ return url.endsWith("/responses") || url.includes("/responses?");
481
+ }
482
+ }
483
+ dumpRawRequest(url, body) {
484
+ console.info(`${RAW_REQUEST_LOG_PREFIX} ${url}`);
485
+ try {
486
+ console.info(JSON.stringify(JSON.parse(body), null, 2));
487
+ }
488
+ catch (_a) {
489
+ console.info(body);
490
+ }
491
+ }
492
+ getResponsesUrl() {
493
+ const baseUrl = this.getConfiguredBaseUrl() || DEFAULT_OPENAI_BASE_URL;
494
+ const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
495
+ return new URL("responses", normalizedBaseUrl).toString();
496
+ }
424
497
  getLangChainAgentSpec(params) {
425
498
  const extraRequestBodyParameters = (this.options.extraRequestBodyParameters || {});
426
499
  const { reasoning } = extraRequestBodyParameters, modelKwargs = __rest(extraRequestBodyParameters, ["reasoning"]);
427
- return {
428
- model: new ChatOpenAI({
429
- model: this.options.model || "gpt-5-nano",
430
- apiKey: this.options.openAiApiKey,
431
- useResponsesApi: true,
432
- maxTokens: params.maxTokens,
433
- reasoning: reasoning !== null && reasoning !== void 0 ? reasoning : {
434
- effort: getAgentReasoningEffort(params.purpose),
435
- summary: "auto",
436
- },
437
- modelKwargs,
500
+ const configuredBaseUrl = this.getConfiguredBaseUrl();
501
+ const normalizedModelKwargs = Object.assign({}, modelKwargs);
502
+ const clientConfiguration = this.getClientConfiguration();
503
+ const useComplitionApi = this.shouldUseComplitionApi();
504
+ const chatOpenAiOptions = {
505
+ model: this.options.model || "gpt-5-nano",
506
+ apiKey: this.options.openAiApiKey,
507
+ maxTokens: params.maxTokens,
508
+ reasoning: buildReasoningConfig({
509
+ reasoning,
510
+ effort: getAgentReasoningEffort(params.purpose),
438
511
  }),
439
- middleware: params.purpose === "primary"
512
+ modelKwargs: normalizedModelKwargs,
513
+ };
514
+ chatOpenAiOptions.useResponsesApi = !useComplitionApi;
515
+ let supportsResponseContinuation = true;
516
+ if (configuredBaseUrl || useComplitionApi) {
517
+ supportsResponseContinuation = false;
518
+ }
519
+ if (clientConfiguration) {
520
+ chatOpenAiOptions.configuration = clientConfiguration;
521
+ }
522
+ return {
523
+ model: new ChatOpenAI(chatOpenAiOptions),
524
+ middleware: params.purpose === "primary" && supportsResponseContinuation
440
525
  ? [createOpenAiResponsesContinuationMiddleware()]
441
526
  : [],
442
527
  };
package/dist/types.d.ts CHANGED
@@ -5,6 +5,21 @@ export interface AdapterOptions {
5
5
  * Set openAiApiKey: process.env.OPENAI_API_KEY to access it
6
6
  */
7
7
  openAiApiKey: string;
8
+ /**
9
+ * Optional OpenAI-compatible base URL.
10
+ *
11
+ * Example: `https://oai.endpoints.kepler.ai.cloud.ovh.net/v1`
12
+ */
13
+ baseUrl?: string;
14
+ /**
15
+ * Forces LangChain agent mode to use the Chat Completions API instead of the
16
+ * Responses API.
17
+ *
18
+ * When omitted, the adapter keeps the current default behavior:
19
+ * - official OpenAI uses the Responses API
20
+ * - custom `baseUrl` providers use the Chat Completions API
21
+ */
22
+ useComplitionApi?: boolean;
8
23
  /**
9
24
  * Model name. Go to https://platform.openai.com/docs/models, select model and copy name.
10
25
  * Default is `gpt-5-nano`.
@@ -14,4 +29,9 @@ export interface AdapterOptions {
14
29
  * Additional request body parameters to include in the API request.
15
30
  */
16
31
  extraRequestBodyParameters?: Record<string, unknown>;
32
+ /**
33
+ * Logs the exact JSON body sent to the OpenAI Responses endpoint.
34
+ * Authorization headers are not logged.
35
+ */
36
+ dumpRawRequest?: boolean;
17
37
  }
package/index.ts CHANGED
@@ -61,6 +61,12 @@ type OpenAiResponsesContext = {
61
61
  turnId: string;
62
62
  };
63
63
 
64
+ const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
65
+ const RAW_REQUEST_LOG_PREFIX = "[CompletionAdapterOpenAIResponses] Raw /responses request";
66
+
67
+ type FetchInput = Parameters<typeof fetch>[0];
68
+ type FetchInit = Parameters<typeof fetch>[1];
69
+
64
70
  function extractOutputText(data: OpenAIResponsesSuccess): string {
65
71
  let text = "";
66
72
 
@@ -147,6 +153,17 @@ function getAgentReasoningEffort(
147
153
  return purpose === "summary" ? "minimal" : "low";
148
154
  }
149
155
 
156
+ function buildReasoningConfig(params: {
157
+ reasoning?: Record<string, unknown>;
158
+ effort: Exclude<ReasoningEffort, "none"> | ReasoningEffort;
159
+ }) {
160
+ return {
161
+ summary: "detailed",
162
+ effort: params.effort,
163
+ ...(params.reasoning ?? {}),
164
+ };
165
+ }
166
+
150
167
  function getTurnKey(context: OpenAiResponsesContext) {
151
168
  return `${context.sessionId}:${context.turnId}`;
152
169
  }
@@ -258,6 +275,86 @@ export default class CompletionAdapterOpenAIResponses
258
275
  return this.encoding.encode(content).length;
259
276
  }
260
277
 
278
+ private getConfiguredBaseUrl() {
279
+ return this.options.baseUrl;
280
+ }
281
+
282
+ private shouldUseComplitionApi() {
283
+ if (typeof this.options.useComplitionApi === "boolean") {
284
+ return this.options.useComplitionApi;
285
+ }
286
+
287
+ return Boolean(this.getConfiguredBaseUrl());
288
+ }
289
+
290
+ private shouldDumpRawRequest() {
291
+ return this.options.dumpRawRequest === true;
292
+ }
293
+
294
+ private getClientConfiguration() {
295
+ const configuredBaseUrl = this.getConfiguredBaseUrl();
296
+ const debugFetch = this.shouldDumpRawRequest()
297
+ ? this.createResponsesDebugFetch()
298
+ : undefined;
299
+
300
+ if (!configuredBaseUrl && !debugFetch) {
301
+ return undefined;
302
+ }
303
+
304
+ return {
305
+ ...(configuredBaseUrl ? { baseURL: configuredBaseUrl } : {}),
306
+ ...(debugFetch ? { fetch: debugFetch } : {}),
307
+ };
308
+ }
309
+
310
+ private createResponsesDebugFetch() {
311
+ return async (input: FetchInput, init?: FetchInit) => {
312
+ const url = this.getFetchUrl(input);
313
+
314
+ if (this.isResponsesUrl(url) && typeof init?.body === "string") {
315
+ this.dumpRawRequest(url, init.body);
316
+ }
317
+
318
+ return fetch(input, init);
319
+ };
320
+ }
321
+
322
+ private getFetchUrl(input: FetchInput) {
323
+ if (typeof input === "string") {
324
+ return input;
325
+ }
326
+
327
+ if (input instanceof URL) {
328
+ return input.toString();
329
+ }
330
+
331
+ return input.url;
332
+ }
333
+
334
+ private isResponsesUrl(url: string) {
335
+ try {
336
+ return new URL(url).pathname.endsWith("/responses");
337
+ } catch {
338
+ return url.endsWith("/responses") || url.includes("/responses?");
339
+ }
340
+ }
341
+
342
+ private dumpRawRequest(url: string, body: string) {
343
+ console.info(`${RAW_REQUEST_LOG_PREFIX} ${url}`);
344
+ try {
345
+ console.info(JSON.stringify(JSON.parse(body), null, 2));
346
+ } catch {
347
+ console.info(body);
348
+ }
349
+ }
350
+
351
+ private getResponsesUrl() {
352
+ const baseUrl = this.getConfiguredBaseUrl() || DEFAULT_OPENAI_BASE_URL;
353
+ const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
354
+
355
+ return new URL("responses", normalizedBaseUrl).toString();
356
+ }
357
+
261
358
  getLangChainAgentSpec(params: {
262
359
  maxTokens: number;
263
360
  purpose: AgentModelPurpose;
@@ -265,23 +362,41 @@ export default class CompletionAdapterOpenAIResponses
265
362
  const extraRequestBodyParameters =
266
363
  (this.options.extraRequestBodyParameters || {}) as Record<string, unknown> & {
267
364
  reasoning?: Record<string, unknown>;
365
+ text?: Record<string, unknown>;
268
366
  };
269
367
  const { reasoning, ...modelKwargs } = extraRequestBodyParameters;
368
+ const configuredBaseUrl = this.getConfiguredBaseUrl();
369
+ const normalizedModelKwargs = { ...modelKwargs };
370
+
371
+
372
+ const clientConfiguration = this.getClientConfiguration();
373
+ const useComplitionApi = this.shouldUseComplitionApi();
374
+ const chatOpenAiOptions: Record<string, unknown> = {
375
+ model: this.options.model || "gpt-5-nano",
376
+ apiKey: this.options.openAiApiKey,
377
+ maxTokens: params.maxTokens,
378
+ reasoning: buildReasoningConfig({
379
+ reasoning,
380
+ effort: getAgentReasoningEffort(params.purpose),
381
+ }),
382
+ modelKwargs: normalizedModelKwargs,
383
+ };
384
+
385
+ chatOpenAiOptions.useResponsesApi = !useComplitionApi;
386
+
387
+ let supportsResponseContinuation = true;
388
+ if (configuredBaseUrl || useComplitionApi) {
389
+ supportsResponseContinuation = false;
390
+ }
391
+
392
+ if (clientConfiguration) {
393
+ chatOpenAiOptions.configuration = clientConfiguration;
394
+ }
270
395
 
271
396
  return {
272
- model: new ChatOpenAI({
273
- model: this.options.model || "gpt-5-nano",
274
- apiKey: this.options.openAiApiKey,
275
- useResponsesApi: true,
276
- maxTokens: params.maxTokens,
277
- reasoning: reasoning ?? {
278
- effort: getAgentReasoningEffort(params.purpose),
279
- summary: "auto",
280
- },
281
- modelKwargs,
282
- } as any),
397
+ model: new ChatOpenAI(chatOpenAiOptions as any),
283
398
  middleware:
284
- params.purpose === "primary"
399
+ params.purpose === "primary" && supportsResponseContinuation
285
400
  ? [createOpenAiResponsesContinuationMiddleware()]
286
401
  : [],
287
402
  };
@@ -323,7 +438,11 @@ export default class CompletionAdapterOpenAIResponses
323
438
  } = request;
324
439
  const model = this.options.model || "gpt-5-nano";
325
440
  const isStreaming = typeof streamChunkCallback === "function";
326
- const extra = this.options.extraRequestBodyParameters;
441
+ const extra =
442
+ this.options.extraRequestBodyParameters as
443
+ | (Record<string, unknown> & { reasoning?: Record<string, unknown> })
444
+ | undefined;
445
+ const { reasoning: extraReasoning, ...extraWithoutReasoning } = extra ?? {};
327
446
  let openAiTools: OpenAITool[] | undefined = undefined;
328
447
  if (tools && tools.length > 0) {
329
448
  openAiTools = tools.map((tool) => ({
@@ -331,7 +450,7 @@ export default class CompletionAdapterOpenAIResponses
331
450
  name: tool.name,
332
451
  description: tool.description,
333
452
  parameters: tool.input_schema,
334
- strict: true,
453
+ strict: false,
335
454
  }));
336
455
  }
337
456
 
@@ -353,20 +472,28 @@ export default class CompletionAdapterOpenAIResponses
353
472
  },
354
473
  },
355
474
  reasoning: {
356
- effort: requestReasoningEffort,
357
- summary: "auto",
475
+ ...buildReasoningConfig({
476
+ reasoning: extraReasoning,
477
+ effort: requestReasoningEffort,
478
+ }),
358
479
  },
359
480
  tools: openAiTools,
360
- ...extra,
481
+ ...extraWithoutReasoning,
361
482
  } as ResponseCreateBody;
362
483
 
363
- const resp = await fetch("https://api.openai.com/v1/responses", {
484
+ const serializedBody = JSON.stringify(body);
485
+
486
+ if (this.shouldDumpRawRequest()) {
487
+ this.dumpRawRequest(this.getResponsesUrl(), serializedBody);
488
+ }
489
+
490
+ const resp = await fetch(this.getResponsesUrl(), {
364
491
  method: "POST",
365
492
  headers: {
366
493
  "Content-Type": "application/json",
367
494
  Authorization: `Bearer ${this.options.openAiApiKey}`,
368
495
  },
369
- body: JSON.stringify(body),
496
+ body: serializedBody,
370
497
  });
371
498
 
372
499
  if (!resp.ok) {
@@ -585,4 +712,4 @@ export default class CompletionAdapterOpenAIResponses
585
712
  reader.releaseLock();
586
713
  }
587
714
  };
588
- }
715
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/completion-adapter-openai-responses",
3
- "version": "2.0.24",
3
+ "version": "2.0.26",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
package/types.ts CHANGED
@@ -6,6 +6,23 @@ export interface AdapterOptions {
6
6
  */
7
7
  openAiApiKey: string;
8
8
 
9
+ /**
10
+ * Optional OpenAI-compatible base URL.
11
+ *
12
+ * Example: `https://oai.endpoints.kepler.ai.cloud.ovh.net/v1`
13
+ */
14
+ baseUrl?: string;
15
+
16
+ /**
17
+ * Forces LangChain agent mode to use the Chat Completions API instead of the
18
+ * Responses API.
19
+ *
20
+ * When omitted, the adapter keeps the current default behavior:
21
+ * - official OpenAI uses the Responses API
22
+ * - custom `baseUrl` providers use the Chat Completions API
23
+ */
24
+ useComplitionApi?: boolean;
25
+
9
26
  /**
10
27
  * Model name. Go to https://platform.openai.com/docs/models, select model and copy name.
11
28
  * Default is `gpt-5-nano`.
@@ -16,4 +33,10 @@ export interface AdapterOptions {
16
33
  * Additional request body parameters to include in the API request.
17
34
  */
18
35
  extraRequestBodyParameters?: Record<string, unknown>;
19
- }
36
+
37
+ /**
38
+ * Logs the exact JSON body sent to the OpenAI Responses endpoint.
39
+ * Authorization headers are not logged.
40
+ */
41
+ dumpRawRequest?: boolean;
42
+ }