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

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
@@ -1,8 +1,10 @@
1
1
  import type { AdapterOptions } from "./types.js";
2
2
  import type { CompletionAdapter, CompletionStreamEvent, CompletionTool } from "adminforth";
3
+ import { ChatOpenAI } from "@langchain/openai";
3
4
  export type { AdapterOptions } from "./types.js";
4
5
  type StreamChunkCallback = (chunk: string, event?: CompletionStreamEvent) => void | Promise<void>;
5
6
  type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
7
+ type AgentModelPurpose = "primary" | "summary";
6
8
  type CompletionRequestInput = {
7
9
  content: string;
8
10
  maxTokens?: number;
@@ -17,6 +19,22 @@ export default class CompletionAdapterOpenAIResponses implements CompletionAdapt
17
19
  constructor(options: AdapterOptions);
18
20
  validate(): void;
19
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;
31
+ getLangChainAgentSpec(params: {
32
+ maxTokens: number;
33
+ purpose: AgentModelPurpose;
34
+ }): {
35
+ model: ChatOpenAI<import("@langchain/openai").ChatOpenAICallOptions>;
36
+ middleware: import("langchain").AgentMiddleware<undefined, undefined, unknown, readonly (import("@langchain/core/tools").ClientTool | import("@langchain/core/tools").ServerTool)[]>[];
37
+ };
20
38
  complete: (requestOrContent: CompletionRequestInput | string, maxTokens?: number, outputSchema?: any, reasoningEffort?: ReasoningEffort, toolsOrOnChunk?: CompletionTool[] | StreamChunkCallback, onChunk?: StreamChunkCallback) => Promise<{
21
39
  content?: string;
22
40
  finishReason?: string;
package/dist/index.js CHANGED
@@ -7,7 +7,23 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ var __rest = (this && this.__rest) || function (s, e) {
11
+ var t = {};
12
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
13
+ t[p] = s[p];
14
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
15
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
16
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
17
+ t[p[i]] = s[p[i]];
18
+ }
19
+ return t;
20
+ };
21
+ import { AIMessage } from "@langchain/core/messages";
22
+ import { ChatOpenAI } from "@langchain/openai";
23
+ import { createMiddleware } from "langchain";
10
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";
11
27
  function extractOutputText(data) {
12
28
  var _a;
13
29
  let text = "";
@@ -80,6 +96,68 @@ function parseSseBlock(block) {
80
96
  }
81
97
  return data ? { event, data } : null;
82
98
  }
99
+ function getAgentReasoningEffort(purpose) {
100
+ return purpose === "summary" ? "minimal" : "low";
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
+ }
106
+ function getTurnKey(context) {
107
+ return `${context.sessionId}:${context.turnId}`;
108
+ }
109
+ function getResponseId(message) {
110
+ var _a;
111
+ const metadata = message.response_metadata;
112
+ return (_a = metadata === null || metadata === void 0 ? void 0 : metadata.id) !== null && _a !== void 0 ? _a : null;
113
+ }
114
+ function getPreviousResponseId(modelSettings) {
115
+ return modelSettings === null || modelSettings === void 0 ? void 0 : modelSettings.previous_response_id;
116
+ }
117
+ function getContinuationMessages(messages, previousResponseId) {
118
+ var _a;
119
+ let continuationStartIndex = null;
120
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
121
+ const message = messages[index];
122
+ if (AIMessage.isInstance(message) &&
123
+ ((_a = message.response_metadata) === null || _a === void 0 ? void 0 : _a.id) ===
124
+ previousResponseId) {
125
+ continuationStartIndex = index + 1;
126
+ break;
127
+ }
128
+ }
129
+ if (continuationStartIndex === null) {
130
+ return null;
131
+ }
132
+ return messages.slice(continuationStartIndex);
133
+ }
134
+ function createOpenAiResponsesContinuationMiddleware() {
135
+ const responseIdsByTurn = new Map();
136
+ return createMiddleware({
137
+ name: "OpenAiResponsesContinuationMiddleware",
138
+ wrapModelCall(request, handler) {
139
+ return __awaiter(this, void 0, void 0, function* () {
140
+ var _a;
141
+ const context = request.runtime.context;
142
+ const turnKey = getTurnKey(context);
143
+ const previousResponseId = (_a = getPreviousResponseId(request.modelSettings)) !== null && _a !== void 0 ? _a : responseIdsByTurn.get(turnKey);
144
+ const continuationMessages = previousResponseId
145
+ ? getContinuationMessages(request.messages, previousResponseId)
146
+ : null;
147
+ const response = (yield handler(previousResponseId && continuationMessages
148
+ ? Object.assign(Object.assign({}, request), { messages: continuationMessages, modelSettings: Object.assign(Object.assign({}, request.modelSettings), { previous_response_id: previousResponseId }) }) : request));
149
+ const responseId = getResponseId(response);
150
+ if (responseId) {
151
+ responseIdsByTurn.set(turnKey, responseId);
152
+ }
153
+ else {
154
+ responseIdsByTurn.delete(turnKey);
155
+ }
156
+ return response;
157
+ });
158
+ },
159
+ });
160
+ }
83
161
  export default class CompletionAdapterOpenAIResponses {
84
162
  constructor(options) {
85
163
  this.complete = (requestOrContent_1, ...args_1) => __awaiter(this, [requestOrContent_1, ...args_1], void 0, function* (requestOrContent, maxTokens = 50, outputSchema, reasoningEffort = "low", toolsOrOnChunk, onChunk) {
@@ -100,6 +178,7 @@ export default class CompletionAdapterOpenAIResponses {
100
178
  const model = this.options.model || "gpt-5-nano";
101
179
  const isStreaming = typeof streamChunkCallback === "function";
102
180
  const extra = this.options.extraRequestBodyParameters;
181
+ const _d = extra !== null && extra !== void 0 ? extra : {}, { reasoning: extraReasoning } = _d, extraWithoutReasoning = __rest(_d, ["reasoning"]);
103
182
  let openAiTools = undefined;
104
183
  if (tools && tools.length > 0) {
105
184
  openAiTools = tools.map((tool) => ({
@@ -107,7 +186,7 @@ export default class CompletionAdapterOpenAIResponses {
107
186
  name: tool.name,
108
187
  description: tool.description,
109
188
  parameters: tool.input_schema,
110
- strict: true,
189
+ strict: false,
111
190
  }));
112
191
  }
113
192
  const body = Object.assign({ model, input: content, max_output_tokens: requestMaxTokens, stream: isStreaming, text: requestOutputSchema
@@ -118,17 +197,21 @@ export default class CompletionAdapterOpenAIResponses {
118
197
  format: {
119
198
  type: "text",
120
199
  },
121
- }, reasoning: {
200
+ }, reasoning: Object.assign({}, buildReasoningConfig({
201
+ reasoning: extraReasoning,
122
202
  effort: requestReasoningEffort,
123
- summary: "auto",
124
- }, tools: openAiTools }, extra);
125
- 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(), {
126
209
  method: "POST",
127
210
  headers: {
128
211
  "Content-Type": "application/json",
129
212
  Authorization: `Bearer ${this.options.openAiApiKey}`,
130
213
  },
131
- body: JSON.stringify(body),
214
+ body: serializedBody,
132
215
  });
133
216
  if (!resp.ok) {
134
217
  let errorMessage = `OpenAI request failed with status ${resp.status}`;
@@ -137,7 +220,7 @@ export default class CompletionAdapterOpenAIResponses {
137
220
  if ((_a = errorData.error) === null || _a === void 0 ? void 0 : _a.message)
138
221
  errorMessage = errorData.error.message;
139
222
  }
140
- catch (_d) { }
223
+ catch (_e) { }
141
224
  return { error: errorMessage };
142
225
  }
143
226
  if (!isStreaming) {
@@ -259,7 +342,7 @@ export default class CompletionAdapterOpenAIResponses {
259
342
  try {
260
343
  event = JSON.parse(parsedBlock.data);
261
344
  }
262
- catch (_e) {
345
+ catch (_f) {
263
346
  continue;
264
347
  }
265
348
  if ((_c = event === null || event === void 0 ? void 0 : event.error) === null || _c === void 0 ? void 0 : _c.message) {
@@ -335,7 +418,9 @@ export default class CompletionAdapterOpenAIResponses {
335
418
  this.encoding = encoding_for_model((this.options.model || "gpt-5-nano"));
336
419
  }
337
420
  catch (error) {
338
- console.warn(`Failed to initialize tiktoken tokenizer for model "${this.options.model}", falling back to "gpt-5-nano". Error:`);
421
+ // console.warn(
422
+ // `Failed to initialize tiktoken tokenizer for model "${this.options.model}", falling back to "gpt-5-nano". Error:`,
423
+ // );
339
424
  this.encoding = encoding_for_model("gpt-5-nano");
340
425
  }
341
426
  }
@@ -347,4 +432,98 @@ export default class CompletionAdapterOpenAIResponses {
347
432
  measureTokensCount(content) {
348
433
  return this.encoding.encode(content).length;
349
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
+ }
497
+ getLangChainAgentSpec(params) {
498
+ const extraRequestBodyParameters = (this.options.extraRequestBodyParameters || {});
499
+ const { reasoning } = extraRequestBodyParameters, modelKwargs = __rest(extraRequestBodyParameters, ["reasoning"]);
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),
511
+ }),
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
525
+ ? [createOpenAiResponsesContinuationMiddleware()]
526
+ : [],
527
+ };
528
+ }
350
529
  }
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
@@ -4,6 +4,9 @@ import type {
4
4
  CompletionStreamEvent,
5
5
  CompletionTool,
6
6
  } from "adminforth";
7
+ import { AIMessage } from "@langchain/core/messages";
8
+ import { ChatOpenAI } from "@langchain/openai";
9
+ import { createMiddleware } from "langchain";
7
10
  import { encoding_for_model, type TiktokenModel } from "tiktoken";
8
11
  import type OpenAI from "openai";
9
12
 
@@ -22,6 +25,8 @@ type ReasoningEffort =
22
25
  | "high"
23
26
  | "xhigh";
24
27
 
28
+ type AgentModelPurpose = "primary" | "summary";
29
+
25
30
  type CompletionRequestInput = {
26
31
  content: string;
27
32
  maxTokens?: number;
@@ -47,6 +52,21 @@ type OpenAIFunctionCall = Extract<
47
52
  { type: "function_call" }
48
53
  >;
49
54
 
55
+ type OpenAiResponsesMetadata = {
56
+ id?: string;
57
+ };
58
+
59
+ type OpenAiResponsesContext = {
60
+ sessionId: string;
61
+ turnId: string;
62
+ };
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
+
50
70
  function extractOutputText(data: OpenAIResponsesSuccess): string {
51
71
  let text = "";
52
72
 
@@ -127,6 +147,104 @@ function parseSseBlock(block: string) {
127
147
  return data ? { event, data } : null;
128
148
  }
129
149
 
150
+ function getAgentReasoningEffort(
151
+ purpose: AgentModelPurpose,
152
+ ): Exclude<ReasoningEffort, "none"> {
153
+ return purpose === "summary" ? "minimal" : "low";
154
+ }
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
+
167
+ function getTurnKey(context: OpenAiResponsesContext) {
168
+ return `${context.sessionId}:${context.turnId}`;
169
+ }
170
+
171
+ function getResponseId(message: AIMessage) {
172
+ const metadata = message.response_metadata as OpenAiResponsesMetadata | undefined;
173
+ return metadata?.id ?? null;
174
+ }
175
+
176
+ function getPreviousResponseId(modelSettings?: Record<string, unknown>) {
177
+ return (modelSettings as { previous_response_id?: string } | undefined)
178
+ ?.previous_response_id;
179
+ }
180
+
181
+ function getContinuationMessages<T extends { response_metadata?: unknown }>(
182
+ messages: T[],
183
+ previousResponseId: string,
184
+ ) {
185
+ let continuationStartIndex: number | null = null;
186
+
187
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
188
+ const message = messages[index];
189
+
190
+ if (
191
+ AIMessage.isInstance(message) &&
192
+ (message.response_metadata as OpenAiResponsesMetadata | undefined)?.id ===
193
+ previousResponseId
194
+ ) {
195
+ continuationStartIndex = index + 1;
196
+ break;
197
+ }
198
+ }
199
+
200
+ if (continuationStartIndex === null) {
201
+ return null;
202
+ }
203
+
204
+ return messages.slice(continuationStartIndex);
205
+ }
206
+
207
+ function createOpenAiResponsesContinuationMiddleware() {
208
+ const responseIdsByTurn = new Map<string, string>();
209
+
210
+ return createMiddleware({
211
+ name: "OpenAiResponsesContinuationMiddleware",
212
+ async wrapModelCall(request, handler) {
213
+ const context = request.runtime.context as OpenAiResponsesContext;
214
+ const turnKey = getTurnKey(context);
215
+ const previousResponseId =
216
+ getPreviousResponseId(request.modelSettings) ??
217
+ responseIdsByTurn.get(turnKey);
218
+ const continuationMessages = previousResponseId
219
+ ? getContinuationMessages(request.messages, previousResponseId)
220
+ : null;
221
+
222
+ const response = (await handler(
223
+ previousResponseId && continuationMessages
224
+ ? {
225
+ ...request,
226
+ messages: continuationMessages,
227
+ modelSettings: {
228
+ ...request.modelSettings,
229
+ previous_response_id: previousResponseId,
230
+ },
231
+ }
232
+ : request,
233
+ )) as AIMessage;
234
+
235
+ const responseId = getResponseId(response);
236
+
237
+ if (responseId) {
238
+ responseIdsByTurn.set(turnKey, responseId);
239
+ } else {
240
+ responseIdsByTurn.delete(turnKey);
241
+ }
242
+
243
+ return response;
244
+ },
245
+ });
246
+ }
247
+
130
248
  export default class CompletionAdapterOpenAIResponses
131
249
  implements CompletionAdapter
132
250
  {
@@ -140,9 +258,9 @@ export default class CompletionAdapterOpenAIResponses
140
258
  (this.options.model || "gpt-5-nano") as TiktokenModel,
141
259
  );
142
260
  } catch (error) {
143
- console.warn(
144
- `Failed to initialize tiktoken tokenizer for model "${this.options.model}", falling back to "gpt-5-nano". Error:`,
145
- );
261
+ // console.warn(
262
+ // `Failed to initialize tiktoken tokenizer for model "${this.options.model}", falling back to "gpt-5-nano". Error:`,
263
+ // );
146
264
  this.encoding = encoding_for_model("gpt-5-nano" as TiktokenModel);
147
265
  }
148
266
  }
@@ -157,6 +275,133 @@ export default class CompletionAdapterOpenAIResponses
157
275
  return this.encoding.encode(content).length;
158
276
  }
159
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
+
358
+ getLangChainAgentSpec(params: {
359
+ maxTokens: number;
360
+ purpose: AgentModelPurpose;
361
+ }) {
362
+ const extraRequestBodyParameters =
363
+ (this.options.extraRequestBodyParameters || {}) as Record<string, unknown> & {
364
+ reasoning?: Record<string, unknown>;
365
+ text?: Record<string, unknown>;
366
+ };
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
+ }
395
+
396
+ return {
397
+ model: new ChatOpenAI(chatOpenAiOptions as any),
398
+ middleware:
399
+ params.purpose === "primary" && supportsResponseContinuation
400
+ ? [createOpenAiResponsesContinuationMiddleware()]
401
+ : [],
402
+ };
403
+ }
404
+
160
405
  complete = async (
161
406
  requestOrContent: CompletionRequestInput | string,
162
407
  maxTokens = 50,
@@ -193,7 +438,11 @@ export default class CompletionAdapterOpenAIResponses
193
438
  } = request;
194
439
  const model = this.options.model || "gpt-5-nano";
195
440
  const isStreaming = typeof streamChunkCallback === "function";
196
- 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 ?? {};
197
446
  let openAiTools: OpenAITool[] | undefined = undefined;
198
447
  if (tools && tools.length > 0) {
199
448
  openAiTools = tools.map((tool) => ({
@@ -201,7 +450,7 @@ export default class CompletionAdapterOpenAIResponses
201
450
  name: tool.name,
202
451
  description: tool.description,
203
452
  parameters: tool.input_schema,
204
- strict: true,
453
+ strict: false,
205
454
  }));
206
455
  }
207
456
 
@@ -223,20 +472,28 @@ export default class CompletionAdapterOpenAIResponses
223
472
  },
224
473
  },
225
474
  reasoning: {
226
- effort: requestReasoningEffort,
227
- summary: "auto",
475
+ ...buildReasoningConfig({
476
+ reasoning: extraReasoning,
477
+ effort: requestReasoningEffort,
478
+ }),
228
479
  },
229
480
  tools: openAiTools,
230
- ...extra,
481
+ ...extraWithoutReasoning,
231
482
  } as ResponseCreateBody;
232
483
 
233
- 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(), {
234
491
  method: "POST",
235
492
  headers: {
236
493
  "Content-Type": "application/json",
237
494
  Authorization: `Bearer ${this.options.openAiApiKey}`,
238
495
  },
239
- body: JSON.stringify(body),
496
+ body: serializedBody,
240
497
  });
241
498
 
242
499
  if (!resp.ok) {
@@ -455,4 +712,4 @@ export default class CompletionAdapterOpenAIResponses
455
712
  reader.releaseLock();
456
713
  }
457
714
  };
458
- }
715
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/completion-adapter-openai-responses",
3
- "version": "2.0.23",
3
+ "version": "2.0.25",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -9,13 +9,16 @@
9
9
  "rollout": "npm run build && npm publish --access public"
10
10
  },
11
11
  "keywords": [],
12
- "author": "",
12
+ "author": "DevForth (https://devforth.io)",
13
13
  "license": "MIT",
14
14
  "description": "AdminForth completion adapter for the OpenAI Responses API.",
15
15
  "devDependencies": {
16
16
  "typescript": "^5.9.3"
17
17
  },
18
18
  "dependencies": {
19
+ "@langchain/core": "^1.1.41",
20
+ "@langchain/openai": "1.4.4",
21
+ "langchain": "^1.3.4",
19
22
  "openai": "^6.34.0",
20
23
  "tiktoken": "^1.0.22"
21
24
  },
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
+ }