@adminforth/completion-adapter-openai-responses 1.0.0 → 1.0.1
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 +20 -0
- package/dist/index.d.ts +8 -34
- package/dist/index.js +15 -531
- package/dist/langchain.d.ts +14 -0
- package/dist/langchain.js +114 -0
- package/dist/openai.d.ts +50 -0
- package/dist/openai.js +349 -0
- package/dist/types.d.ts +3 -1
- package/index.ts +28 -712
- package/langchain.ts +169 -0
- package/openai.ts +467 -0
- package/package.json +2 -2
- package/types.ts +7 -1
package/index.ts
CHANGED
|
@@ -1,289 +1,35 @@
|
|
|
1
1
|
import type { AdapterOptions } from "./types.js";
|
|
2
2
|
import type {
|
|
3
3
|
CompletionAdapter,
|
|
4
|
-
CompletionStreamEvent,
|
|
5
4
|
CompletionTool,
|
|
6
5
|
} from "adminforth";
|
|
7
|
-
import { AIMessage } from "@langchain/core/messages";
|
|
8
|
-
import { ChatOpenAI } from "@langchain/openai";
|
|
9
|
-
import { createMiddleware } from "langchain";
|
|
10
6
|
import { encoding_for_model, type TiktokenModel } from "tiktoken";
|
|
11
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
OpenAIResponsesService,
|
|
9
|
+
type CompletionRequestInput,
|
|
10
|
+
type CompletionResult,
|
|
11
|
+
type ReasoningEffort,
|
|
12
|
+
type StreamChunkCallback,
|
|
13
|
+
} from "./openai.js";
|
|
14
|
+
import {
|
|
15
|
+
createLangChainAgentSpec,
|
|
16
|
+
type AgentModelPurpose,
|
|
17
|
+
} from "./langchain.js";
|
|
12
18
|
|
|
13
19
|
export type { AdapterOptions } from "./types.js";
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
chunk: string,
|
|
17
|
-
event?: CompletionStreamEvent,
|
|
18
|
-
) => void | Promise<void>;
|
|
19
|
-
|
|
20
|
-
type ReasoningEffort =
|
|
21
|
-
| "none"
|
|
22
|
-
| "minimal"
|
|
23
|
-
| "low"
|
|
24
|
-
| "medium"
|
|
25
|
-
| "high"
|
|
26
|
-
| "xhigh";
|
|
27
|
-
|
|
28
|
-
type AgentModelPurpose = "primary" | "summary";
|
|
29
|
-
|
|
30
|
-
type CompletionRequestInput = {
|
|
31
|
-
content: string;
|
|
32
|
-
maxTokens?: number;
|
|
33
|
-
outputSchema?: any;
|
|
34
|
-
reasoningEffort?: ReasoningEffort;
|
|
35
|
-
tools?: CompletionTool[];
|
|
36
|
-
onChunk?: StreamChunkCallback;
|
|
37
|
-
signal?: AbortSignal;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
type ResponseCreateBody = OpenAI.Responses.ResponseCreateParams;
|
|
41
|
-
type OpenAIResponsesSuccess = OpenAI.Responses.Response;
|
|
42
|
-
type OpenAIErrorResponse = {
|
|
43
|
-
error?: {
|
|
44
|
-
message?: string;
|
|
45
|
-
type?: string;
|
|
46
|
-
param?: string | null;
|
|
47
|
-
code?: string | null;
|
|
48
|
-
};
|
|
49
|
-
};
|
|
50
|
-
type OpenAITool = OpenAI.Responses.Tool;
|
|
51
|
-
type OpenAIFunctionCall = Extract<
|
|
52
|
-
OpenAI.Responses.ResponseOutputItem,
|
|
53
|
-
{ type: "function_call" }
|
|
54
|
-
>;
|
|
55
|
-
|
|
56
|
-
type OpenAiResponsesMetadata = {
|
|
57
|
-
id?: string;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
type OpenAiResponsesContext = {
|
|
61
|
-
sessionId: string;
|
|
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;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
80
|
-
const RAW_REQUEST_LOG_PREFIX = "[CompletionAdapterOpenAIResponses] Raw /responses request";
|
|
81
|
-
|
|
82
|
-
type FetchInput = Parameters<typeof fetch>[0];
|
|
83
|
-
type FetchInit = Parameters<typeof fetch>[1];
|
|
84
|
-
|
|
85
|
-
function extractOutputText(data: OpenAIResponsesSuccess): string {
|
|
86
|
-
let text = "";
|
|
87
|
-
|
|
88
|
-
for (const item of data.output ?? []) {
|
|
89
|
-
if (item.type !== "message" || !Array.isArray(item.content)) continue;
|
|
90
|
-
for (const part of item.content) {
|
|
91
|
-
if (part.type === "output_text" && typeof part.text === "string") {
|
|
92
|
-
text += part.text;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return text;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function extractReasoning(data: OpenAIResponsesSuccess): string | undefined {
|
|
101
|
-
let reasoning = "";
|
|
102
|
-
|
|
103
|
-
for (const item of data.output ?? []) {
|
|
104
|
-
if (item.type !== "reasoning") continue;
|
|
105
|
-
|
|
106
|
-
for (const part of item.summary ?? []) {
|
|
107
|
-
if (part?.type === "summary_text" && typeof part.text === "string") {
|
|
108
|
-
reasoning += part.text;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (!reasoning) {
|
|
113
|
-
for (const part of item.content ?? []) {
|
|
114
|
-
if (part?.type === "reasoning_text" && typeof part.text === "string") {
|
|
115
|
-
reasoning += part.text;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return reasoning || undefined;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function extractFunctionCall(
|
|
125
|
-
data: OpenAIResponsesSuccess,
|
|
126
|
-
): OpenAIFunctionCall | undefined {
|
|
127
|
-
for (const item of data.output ?? []) {
|
|
128
|
-
if (item.type === "function_call") {
|
|
129
|
-
return item;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
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
|
-
|
|
151
|
-
async function executeToolCall(
|
|
152
|
-
toolCall: OpenAIFunctionCall,
|
|
153
|
-
tools?: CompletionTool[],
|
|
154
|
-
): Promise<string> {
|
|
155
|
-
const tool = tools?.find((candidate) => candidate.name === toolCall.name);
|
|
156
|
-
if (!tool) {
|
|
157
|
-
throw new Error(`Tool "${toolCall.name}" not found`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const toolResult = await tool.handler(JSON.parse(toolCall.arguments));
|
|
161
|
-
if (typeof toolResult === "string") return toolResult;
|
|
162
|
-
if (typeof toolResult === "undefined") return "";
|
|
163
|
-
return JSON.stringify(toolResult);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function parseSseBlock(block: string) {
|
|
167
|
-
let event: string | undefined;
|
|
168
|
-
let data = "";
|
|
169
|
-
|
|
170
|
-
for (const rawLine of block.split("\n")) {
|
|
171
|
-
const line = rawLine.trimEnd();
|
|
172
|
-
if (!line) continue;
|
|
173
|
-
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
174
|
-
if (line.startsWith("data:")) data += line.slice(5).trim();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return data ? { event, data } : null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function getAgentReasoningEffort(
|
|
181
|
-
purpose: AgentModelPurpose,
|
|
182
|
-
): Exclude<ReasoningEffort, "none"> {
|
|
183
|
-
return purpose === "summary" ? "minimal" : "low";
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function buildReasoningConfig(params: {
|
|
187
|
-
reasoning?: Record<string, unknown>;
|
|
188
|
-
effort: Exclude<ReasoningEffort, "none"> | ReasoningEffort;
|
|
189
|
-
}) {
|
|
190
|
-
return {
|
|
191
|
-
summary: "auto",
|
|
192
|
-
effort: params.effort,
|
|
193
|
-
...(params.reasoning ?? {}),
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function getTurnKey(context: OpenAiResponsesContext) {
|
|
198
|
-
return `${context.sessionId}:${context.turnId}`;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function getResponseId(message: AIMessage) {
|
|
202
|
-
const metadata = message.response_metadata as OpenAiResponsesMetadata | undefined;
|
|
203
|
-
return metadata?.id ?? null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function getPreviousResponseId(modelSettings?: Record<string, unknown>) {
|
|
207
|
-
return (modelSettings as { previous_response_id?: string } | undefined)
|
|
208
|
-
?.previous_response_id;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function getContinuationMessages<T extends { response_metadata?: unknown }>(
|
|
212
|
-
messages: T[],
|
|
213
|
-
previousResponseId: string,
|
|
214
|
-
) {
|
|
215
|
-
let continuationStartIndex: number | null = null;
|
|
216
|
-
|
|
217
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
218
|
-
const message = messages[index];
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
AIMessage.isInstance(message) &&
|
|
222
|
-
(message.response_metadata as OpenAiResponsesMetadata | undefined)?.id ===
|
|
223
|
-
previousResponseId
|
|
224
|
-
) {
|
|
225
|
-
continuationStartIndex = index + 1;
|
|
226
|
-
break;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (continuationStartIndex === null) {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return messages.slice(continuationStartIndex);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function createOpenAiResponsesContinuationMiddleware() {
|
|
238
|
-
const responseIdsByTurn = new Map<string, string>();
|
|
239
|
-
|
|
240
|
-
return createMiddleware({
|
|
241
|
-
name: "OpenAiResponsesContinuationMiddleware",
|
|
242
|
-
async wrapModelCall(request, handler) {
|
|
243
|
-
const context = request.runtime.context as OpenAiResponsesContext;
|
|
244
|
-
const turnKey = getTurnKey(context);
|
|
245
|
-
const previousResponseId =
|
|
246
|
-
getPreviousResponseId(request.modelSettings) ??
|
|
247
|
-
responseIdsByTurn.get(turnKey);
|
|
248
|
-
const continuationMessages = previousResponseId
|
|
249
|
-
? getContinuationMessages(request.messages, previousResponseId)
|
|
250
|
-
: null;
|
|
251
|
-
|
|
252
|
-
const response = (await handler(
|
|
253
|
-
previousResponseId && continuationMessages
|
|
254
|
-
? {
|
|
255
|
-
...request,
|
|
256
|
-
messages: continuationMessages,
|
|
257
|
-
modelSettings: {
|
|
258
|
-
...request.modelSettings,
|
|
259
|
-
previous_response_id: previousResponseId,
|
|
260
|
-
},
|
|
261
|
-
}
|
|
262
|
-
: request,
|
|
263
|
-
)) as AIMessage;
|
|
264
|
-
|
|
265
|
-
const responseId = getResponseId(response);
|
|
266
|
-
|
|
267
|
-
if (responseId) {
|
|
268
|
-
responseIdsByTurn.set(turnKey, responseId);
|
|
269
|
-
} else {
|
|
270
|
-
responseIdsByTurn.delete(turnKey);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return response;
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export default class CompletionAdapterOpenAIResponses
|
|
21
|
+
class CompletionAdapterOpenAIResponses
|
|
279
22
|
implements CompletionAdapter
|
|
280
23
|
{
|
|
281
24
|
options: AdapterOptions;
|
|
282
25
|
private encoding: ReturnType<typeof encoding_for_model>;
|
|
283
26
|
private activeAbortController: AbortController | null = null;
|
|
27
|
+
private openAi: OpenAIResponsesService;
|
|
284
28
|
|
|
285
29
|
constructor(options: AdapterOptions) {
|
|
286
30
|
this.options = options;
|
|
31
|
+
this.openAi = new OpenAIResponsesService(options);
|
|
32
|
+
|
|
287
33
|
try {
|
|
288
34
|
this.encoding = encoding_for_model(
|
|
289
35
|
(this.options.model || "gpt-5-nano") as TiktokenModel,
|
|
@@ -326,119 +72,18 @@ export default class CompletionAdapterOpenAIResponses
|
|
|
326
72
|
return Boolean(this.getConfiguredBaseUrl());
|
|
327
73
|
}
|
|
328
74
|
|
|
329
|
-
private shouldDumpRawRequest() {
|
|
330
|
-
return this.options.dumpRawRequest === true;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
private getClientConfiguration() {
|
|
334
|
-
const configuredBaseUrl = this.getConfiguredBaseUrl();
|
|
335
|
-
const debugFetch = this.shouldDumpRawRequest()
|
|
336
|
-
? this.createResponsesDebugFetch()
|
|
337
|
-
: undefined;
|
|
338
|
-
|
|
339
|
-
if (!configuredBaseUrl && !debugFetch) {
|
|
340
|
-
return undefined;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return {
|
|
344
|
-
...(configuredBaseUrl ? { baseURL: configuredBaseUrl } : {}),
|
|
345
|
-
...(debugFetch ? { fetch: debugFetch } : {}),
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
private createResponsesDebugFetch() {
|
|
350
|
-
return async (input: FetchInput, init?: FetchInit) => {
|
|
351
|
-
const url = this.getFetchUrl(input);
|
|
352
|
-
|
|
353
|
-
if (this.isResponsesUrl(url) && typeof init?.body === "string") {
|
|
354
|
-
this.dumpRawRequest(url, init.body);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return fetch(input, init);
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
private getFetchUrl(input: FetchInput) {
|
|
362
|
-
if (typeof input === "string") {
|
|
363
|
-
return input;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (input instanceof URL) {
|
|
367
|
-
return input.toString();
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return input.url;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
private isResponsesUrl(url: string) {
|
|
374
|
-
try {
|
|
375
|
-
return new URL(url).pathname.endsWith("/responses");
|
|
376
|
-
} catch {
|
|
377
|
-
return url.endsWith("/responses") || url.includes("/responses?");
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
private dumpRawRequest(url: string, body: string) {
|
|
382
|
-
console.info(`${RAW_REQUEST_LOG_PREFIX} ${url}`);
|
|
383
|
-
try {
|
|
384
|
-
console.info(JSON.stringify(JSON.parse(body), null, 2));
|
|
385
|
-
} catch {
|
|
386
|
-
console.info(body);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
private getResponsesUrl() {
|
|
391
|
-
const baseUrl = this.getConfiguredBaseUrl() || DEFAULT_OPENAI_BASE_URL;
|
|
392
|
-
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
393
|
-
|
|
394
|
-
return new URL("responses", normalizedBaseUrl).toString();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
75
|
getLangChainAgentSpec(params: {
|
|
398
76
|
maxTokens: number;
|
|
399
77
|
purpose: AgentModelPurpose;
|
|
400
78
|
}) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
reasoning?: Record<string, unknown>;
|
|
404
|
-
text?: Record<string, unknown>;
|
|
405
|
-
};
|
|
406
|
-
const { reasoning, ...modelKwargs } = extraRequestBodyParameters;
|
|
407
|
-
const configuredBaseUrl = this.getConfiguredBaseUrl();
|
|
408
|
-
const normalizedModelKwargs = { ...modelKwargs };
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const clientConfiguration = this.getClientConfiguration();
|
|
412
|
-
const useComplitionApi = this.shouldUseComplitionApi();
|
|
413
|
-
const chatOpenAiOptions: Record<string, unknown> = {
|
|
414
|
-
model: this.options.model || "gpt-5-nano",
|
|
415
|
-
apiKey: this.options.openAiApiKey,
|
|
79
|
+
return createLangChainAgentSpec({
|
|
80
|
+
options: this.options,
|
|
416
81
|
maxTokens: params.maxTokens,
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
chatOpenAiOptions.useResponsesApi = !useComplitionApi;
|
|
425
|
-
|
|
426
|
-
let supportsResponseContinuation = true;
|
|
427
|
-
if (configuredBaseUrl || useComplitionApi) {
|
|
428
|
-
supportsResponseContinuation = false;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (clientConfiguration) {
|
|
432
|
-
chatOpenAiOptions.configuration = clientConfiguration;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return {
|
|
436
|
-
model: new ChatOpenAI(chatOpenAiOptions as any),
|
|
437
|
-
middleware:
|
|
438
|
-
params.purpose === "primary" && supportsResponseContinuation
|
|
439
|
-
? [createOpenAiResponsesContinuationMiddleware()]
|
|
440
|
-
: [],
|
|
441
|
-
};
|
|
82
|
+
purpose: params.purpose,
|
|
83
|
+
configuredBaseUrl: this.getConfiguredBaseUrl(),
|
|
84
|
+
clientConfiguration: this.openAi.getClientConfiguration(),
|
|
85
|
+
useComplitionApi: this.shouldUseComplitionApi(),
|
|
86
|
+
});
|
|
442
87
|
}
|
|
443
88
|
|
|
444
89
|
complete = async (
|
|
@@ -463,66 +108,7 @@ export default class CompletionAdapterOpenAIResponses
|
|
|
463
108
|
: onChunk,
|
|
464
109
|
}
|
|
465
110
|
: requestOrContent;
|
|
466
|
-
const {
|
|
467
|
-
content,
|
|
468
|
-
maxTokens: requestMaxTokens = 50,
|
|
469
|
-
outputSchema: requestOutputSchema,
|
|
470
|
-
reasoningEffort: requestReasoningEffort = "low",
|
|
471
|
-
tools,
|
|
472
|
-
onChunk: streamChunkCallback,
|
|
473
|
-
signal: requestSignal,
|
|
474
|
-
} = request;
|
|
475
|
-
const model = this.options.model || "gpt-5-nano";
|
|
476
|
-
const isStreaming = typeof streamChunkCallback === "function";
|
|
477
|
-
const extra =
|
|
478
|
-
this.options.extraRequestBodyParameters as
|
|
479
|
-
| (Record<string, unknown> & { reasoning?: Record<string, unknown> })
|
|
480
|
-
| undefined;
|
|
481
|
-
const { reasoning: extraReasoning, ...extraWithoutReasoning } = extra ?? {};
|
|
482
|
-
let openAiTools: OpenAITool[] | undefined = undefined;
|
|
483
|
-
if (tools && tools.length > 0) {
|
|
484
|
-
openAiTools = tools.map((tool) => ({
|
|
485
|
-
type: "function",
|
|
486
|
-
name: tool.name,
|
|
487
|
-
description: tool.description,
|
|
488
|
-
parameters: tool.input_schema,
|
|
489
|
-
strict: false,
|
|
490
|
-
}));
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const body = {
|
|
494
|
-
model,
|
|
495
|
-
input: content,
|
|
496
|
-
max_output_tokens: requestMaxTokens,
|
|
497
|
-
stream: isStreaming,
|
|
498
|
-
text: requestOutputSchema
|
|
499
|
-
? {
|
|
500
|
-
format: {
|
|
501
|
-
type: "json_schema",
|
|
502
|
-
...requestOutputSchema,
|
|
503
|
-
},
|
|
504
|
-
}
|
|
505
|
-
: {
|
|
506
|
-
format: {
|
|
507
|
-
type: "text",
|
|
508
|
-
},
|
|
509
|
-
},
|
|
510
|
-
reasoning: {
|
|
511
|
-
...buildReasoningConfig({
|
|
512
|
-
reasoning: extraReasoning,
|
|
513
|
-
effort: requestReasoningEffort,
|
|
514
|
-
}),
|
|
515
|
-
},
|
|
516
|
-
tools: openAiTools,
|
|
517
|
-
...extraWithoutReasoning,
|
|
518
|
-
} as ResponseCreateBody;
|
|
519
|
-
|
|
520
|
-
const serializedBody = JSON.stringify(body);
|
|
521
|
-
|
|
522
|
-
if (this.shouldDumpRawRequest()) {
|
|
523
|
-
this.dumpRawRequest(this.getResponsesUrl(), serializedBody);
|
|
524
|
-
}
|
|
525
|
-
|
|
111
|
+
const { signal: requestSignal } = request;
|
|
526
112
|
const abortController = new AbortController();
|
|
527
113
|
this.activeAbortController = abortController;
|
|
528
114
|
const abortFromRequestSignal = () => abortController.abort(requestSignal?.reason);
|
|
@@ -533,286 +119,16 @@ export default class CompletionAdapterOpenAIResponses
|
|
|
533
119
|
requestSignal?.addEventListener("abort", abortFromRequestSignal, { once: true });
|
|
534
120
|
}
|
|
535
121
|
|
|
536
|
-
let resp: Response | null = null;
|
|
537
122
|
try {
|
|
538
|
-
|
|
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;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
if (abortController.signal.aborted) {
|
|
553
|
-
return {
|
|
554
|
-
error: error?.message || "Generation aborted",
|
|
555
|
-
finishReason: "aborted",
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
return {
|
|
560
|
-
error: error?.message || "OpenAI request failed",
|
|
561
|
-
};
|
|
123
|
+
return await this.openAi.complete(request, abortController.signal);
|
|
562
124
|
} finally {
|
|
563
125
|
requestSignal?.removeEventListener("abort", abortFromRequestSignal);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
if (!resp) {
|
|
567
|
-
if (this.activeAbortController === abortController) {
|
|
568
|
-
this.activeAbortController = null;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
return {
|
|
572
|
-
error: "OpenAI request failed",
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
|
|
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
|
-
}
|
|
585
|
-
|
|
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;
|
|
637
|
-
|
|
638
|
-
const handleEvent = async (event: any, eventType?: string) => {
|
|
639
|
-
const type = event?.type || eventType;
|
|
640
|
-
|
|
641
|
-
if (type === "response.output_text.delta") {
|
|
642
|
-
const delta = event?.delta || "";
|
|
643
|
-
if (!delta) return;
|
|
644
|
-
fullContent += delta;
|
|
645
|
-
await streamChunkCallback?.(delta, { type: "output", delta, text: fullContent });
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (
|
|
650
|
-
type === "response.reasoning_summary_text.delta" ||
|
|
651
|
-
type === "response.reasoning_text.delta"
|
|
652
|
-
) {
|
|
653
|
-
const delta = event?.delta || "";
|
|
654
|
-
if (!delta) return;
|
|
655
|
-
fullReasoning += delta;
|
|
656
|
-
await streamChunkCallback?.(delta, {
|
|
657
|
-
type: "reasoning",
|
|
658
|
-
delta,
|
|
659
|
-
text: fullReasoning,
|
|
660
|
-
});
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (type === "response.completed" || type === "response.incomplete") {
|
|
665
|
-
const response = event?.response as OpenAIResponsesSuccess | undefined;
|
|
666
|
-
if (!response) return;
|
|
667
|
-
|
|
668
|
-
const finalContent = extractOutputText(response);
|
|
669
|
-
if (finalContent.startsWith(fullContent)) {
|
|
670
|
-
const delta = finalContent.slice(fullContent.length);
|
|
671
|
-
if (delta) {
|
|
672
|
-
fullContent = finalContent;
|
|
673
|
-
await streamChunkCallback?.(delta, {
|
|
674
|
-
type: "output",
|
|
675
|
-
delta,
|
|
676
|
-
text: fullContent,
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
const finalReasoning = extractReasoning(response) || "";
|
|
682
|
-
if (finalReasoning.startsWith(fullReasoning)) {
|
|
683
|
-
const delta = finalReasoning.slice(fullReasoning.length);
|
|
684
|
-
if (delta) {
|
|
685
|
-
fullReasoning = finalReasoning;
|
|
686
|
-
await streamChunkCallback?.(delta, {
|
|
687
|
-
type: "reasoning",
|
|
688
|
-
delta,
|
|
689
|
-
text: fullReasoning,
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
finishReason =
|
|
695
|
-
response.incomplete_details?.reason || response.status || finishReason;
|
|
696
|
-
completedResponse = response;
|
|
697
|
-
usedTokens = extractUsedTokens(response);
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
if (type === "response.failed") {
|
|
702
|
-
throw new Error(
|
|
703
|
-
event?.response?.error?.message ||
|
|
704
|
-
event?.error?.message ||
|
|
705
|
-
"Response failed",
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
try {
|
|
711
|
-
while (true) {
|
|
712
|
-
const { value, done } = await reader.read();
|
|
713
|
-
if (done) break;
|
|
714
|
-
|
|
715
|
-
buffer += decoder.decode(value, { stream: true });
|
|
716
|
-
|
|
717
|
-
const blocks = buffer.split("\n\n");
|
|
718
|
-
buffer = blocks.pop() || "";
|
|
719
|
-
|
|
720
|
-
for (const block of blocks) {
|
|
721
|
-
const parsedBlock = parseSseBlock(block);
|
|
722
|
-
if (!parsedBlock?.data || parsedBlock.data === "[DONE]") continue;
|
|
723
|
-
|
|
724
|
-
let event: any;
|
|
725
|
-
try {
|
|
726
|
-
event = JSON.parse(parsedBlock.data);
|
|
727
|
-
} catch {
|
|
728
|
-
continue;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
if (event?.error?.message) {
|
|
732
|
-
return { error: event.error.message };
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
await handleEvent(event, parsedBlock.event);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (buffer.trim()) {
|
|
740
|
-
const parsedBlock = parseSseBlock(buffer.trim());
|
|
741
|
-
if (parsedBlock?.data && parsedBlock.data !== "[DONE]") {
|
|
742
|
-
try {
|
|
743
|
-
await handleEvent(JSON.parse(parsedBlock.data), parsedBlock.event);
|
|
744
|
-
} catch (error: any) {
|
|
745
|
-
return {
|
|
746
|
-
error: error?.message || "Streaming failed",
|
|
747
|
-
content: fullContent || undefined,
|
|
748
|
-
finishReason,
|
|
749
|
-
};
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
if (completedResponse) {
|
|
755
|
-
const toolCall = extractFunctionCall(completedResponse);
|
|
756
|
-
if (toolCall) {
|
|
757
|
-
try {
|
|
758
|
-
const toolResult = await executeToolCall(toolCall, tools);
|
|
759
|
-
if (toolResult) {
|
|
760
|
-
const delta = toolResult.startsWith(fullContent)
|
|
761
|
-
? toolResult.slice(fullContent.length)
|
|
762
|
-
: toolResult;
|
|
763
|
-
if (delta) {
|
|
764
|
-
await streamChunkCallback?.(delta, {
|
|
765
|
-
type: "output",
|
|
766
|
-
delta,
|
|
767
|
-
text: toolResult,
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
return {
|
|
773
|
-
content: toolResult,
|
|
774
|
-
finishReason: "tool_call",
|
|
775
|
-
used_tokens: usedTokens,
|
|
776
|
-
};
|
|
777
|
-
} catch (error: any) {
|
|
778
|
-
return {
|
|
779
|
-
error: error?.message || "Tool execution failed",
|
|
780
|
-
content: fullContent || undefined,
|
|
781
|
-
finishReason: "tool_call",
|
|
782
|
-
used_tokens: usedTokens,
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
return {
|
|
789
|
-
content: fullContent || undefined,
|
|
790
|
-
finishReason,
|
|
791
|
-
used_tokens: usedTokens,
|
|
792
|
-
};
|
|
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
|
-
|
|
803
|
-
return {
|
|
804
|
-
error: error?.message || "Streaming failed",
|
|
805
|
-
content: fullContent || undefined,
|
|
806
|
-
finishReason,
|
|
807
|
-
used_tokens: usedTokens,
|
|
808
|
-
};
|
|
809
|
-
} finally {
|
|
810
|
-
reader.releaseLock();
|
|
811
|
-
}
|
|
812
|
-
} finally {
|
|
813
126
|
if (this.activeAbortController === abortController) {
|
|
814
127
|
this.activeAbortController = null;
|
|
815
128
|
}
|
|
816
129
|
}
|
|
817
130
|
};
|
|
818
|
-
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export { CompletionAdapterOpenAIResponses };
|
|
134
|
+
export default CompletionAdapterOpenAIResponses;
|