@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 +13 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +188 -9
- package/dist/types.d.ts +20 -0
- package/index.ts +268 -11
- package/package.json +5 -2
- package/types.ts +24 -1
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:
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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:
|
|
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 (
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
227
|
-
|
|
475
|
+
...buildReasoningConfig({
|
|
476
|
+
reasoning: extraReasoning,
|
|
477
|
+
effort: requestReasoningEffort,
|
|
478
|
+
}),
|
|
228
479
|
},
|
|
229
480
|
tools: openAiTools,
|
|
230
|
-
...
|
|
481
|
+
...extraWithoutReasoning,
|
|
231
482
|
} as ResponseCreateBody;
|
|
232
483
|
|
|
233
|
-
const
|
|
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:
|
|
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.
|
|
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
|
+
}
|