@purista/harness-openai 1.2.3 → 1.2.4
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 +31 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +329 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,37 @@ Configure the provider with an OpenAI API key in your application environment.
|
|
|
12
12
|
The adapter is designed for use through the typed `@purista/harness` model
|
|
13
13
|
provider port.
|
|
14
14
|
|
|
15
|
+
```ts
|
|
16
|
+
import { openai } from '@purista/harness-openai'
|
|
17
|
+
|
|
18
|
+
const provider = openai({
|
|
19
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
20
|
+
baseURL: process.env.OPENAI_BASE_URL
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
By default, generation uses Chat Completions for compatibility with
|
|
25
|
+
OpenAI-compatible endpoints:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
openai({ apiKey: process.env.OPENAI_API_KEY! })
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use the Responses API for OpenAI reasoning models that require function tools
|
|
32
|
+
and reasoning effort on `/v1/responses`, such as `gpt-5.5`:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
openai({
|
|
36
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
37
|
+
api: 'responses'
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
When Chat Completions is used with tools and `providerOptions.reasoning_effort`,
|
|
42
|
+
the adapter drops `reasoning_effort` and emits a warning instead of sending a
|
|
43
|
+
request that OpenAI rejects. Use `api: 'responses'` when you need reasoning
|
|
44
|
+
effort and tool calls together.
|
|
45
|
+
|
|
15
46
|
## Package Format
|
|
16
47
|
|
|
17
48
|
This package is ESM-only and ships compiled JavaScript plus TypeScript
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,14 @@ import { type ClientOptions } from 'openai';
|
|
|
4
4
|
* Configuration for the OpenAI model provider factory.
|
|
5
5
|
*/
|
|
6
6
|
export interface OpenAiFactoryOptions extends ClientOptions {
|
|
7
|
+
/**
|
|
8
|
+
* OpenAI API surface used for text/object generation.
|
|
9
|
+
*
|
|
10
|
+
* Use `responses` for reasoning models that require the Responses API for
|
|
11
|
+
* function tools with reasoning effort, such as `gpt-5.5`. The default keeps
|
|
12
|
+
* existing OpenAI-compatible chat-completions endpoints working.
|
|
13
|
+
*/
|
|
14
|
+
api?: 'chat_completions' | 'responses';
|
|
7
15
|
/** Optional injected client for tests or custom transport behavior. */
|
|
8
16
|
client?: OpenAiClient;
|
|
9
17
|
/** Optional adapter-level logger override. Defaults to the harness logger when registered. */
|
|
@@ -18,7 +26,7 @@ export interface OpenAiFactoryOptions extends ClientOptions {
|
|
|
18
26
|
*
|
|
19
27
|
* Execution model:
|
|
20
28
|
* - In-process adapter code
|
|
21
|
-
* - External network calls to OpenAI
|
|
29
|
+
* - External network calls to OpenAI chat completions or responses endpoint
|
|
22
30
|
* - AsyncIterable streaming for `textStream` and `objectStream`
|
|
23
31
|
*
|
|
24
32
|
* @example
|
|
@@ -63,6 +71,11 @@ export type OpenAiClient = {
|
|
|
63
71
|
}): Promise<any>;
|
|
64
72
|
};
|
|
65
73
|
};
|
|
74
|
+
responses?: {
|
|
75
|
+
create(payload: unknown, options?: {
|
|
76
|
+
signal?: AbortSignal;
|
|
77
|
+
}): Promise<any>;
|
|
78
|
+
};
|
|
66
79
|
embeddings: {
|
|
67
80
|
create(payload: unknown, options?: {
|
|
68
81
|
signal?: AbortSignal;
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import OpenAI, {} from 'openai';
|
|
|
5
5
|
*
|
|
6
6
|
* Execution model:
|
|
7
7
|
* - In-process adapter code
|
|
8
|
-
* - External network calls to OpenAI
|
|
8
|
+
* - External network calls to OpenAI chat completions or responses endpoint
|
|
9
9
|
* - AsyncIterable streaming for `textStream` and `objectStream`
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
@@ -60,12 +60,20 @@ class OpenAiModelProvider extends BaseModelProvider {
|
|
|
60
60
|
}
|
|
61
61
|
async doText(req) {
|
|
62
62
|
req.signal.throwIfAborted();
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
if (this.options.api === 'responses') {
|
|
64
|
+
const response = await createResponse(this.client, req, false);
|
|
65
|
+
return mapResponsesTextResponse(response, req);
|
|
66
|
+
}
|
|
67
|
+
const response = await createChatCompletion(this.client, req, false, this.getLogger());
|
|
68
|
+
return mapChatTextResponse(response, req);
|
|
65
69
|
}
|
|
66
70
|
async *doTextStream(req) {
|
|
67
71
|
req.signal.throwIfAborted();
|
|
68
|
-
|
|
72
|
+
if (this.options.api === 'responses') {
|
|
73
|
+
yield* streamResponsesText(this.client, req);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const stream = await createChatCompletion(this.client, req, true, this.getLogger());
|
|
69
77
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
70
78
|
let finishReason = 'stop';
|
|
71
79
|
const toolState = new Map();
|
|
@@ -95,9 +103,21 @@ class OpenAiModelProvider extends BaseModelProvider {
|
|
|
95
103
|
}
|
|
96
104
|
async doObject(req) {
|
|
97
105
|
req.signal.throwIfAborted();
|
|
98
|
-
|
|
106
|
+
if (this.options.api === 'responses') {
|
|
107
|
+
const response = await createResponse(this.client, req, false);
|
|
108
|
+
const content = extractResponsesText(response);
|
|
109
|
+
const toolCalls = extractResponsesToolCalls(response, req, 'object');
|
|
110
|
+
return {
|
|
111
|
+
object: parseJson(content || '{}', req, 'object'),
|
|
112
|
+
...(toolCalls ? { toolCalls } : {}),
|
|
113
|
+
usage: toResponsesUsage(response.usage),
|
|
114
|
+
finishReason: toResponsesFinishReason(response),
|
|
115
|
+
raw: response
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const response = await createChatCompletion(this.client, req, false, this.getLogger());
|
|
99
119
|
const textContent = response.choices[0]?.message?.content ?? '{}';
|
|
100
|
-
const toolCalls =
|
|
120
|
+
const toolCalls = extractChatToolCalls(response, req, 'object');
|
|
101
121
|
return {
|
|
102
122
|
object: parseJson(textContent, req, 'object'),
|
|
103
123
|
...(toolCalls ? { toolCalls } : {}),
|
|
@@ -112,7 +132,11 @@ class OpenAiModelProvider extends BaseModelProvider {
|
|
|
112
132
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
113
133
|
let finishReason = 'stop';
|
|
114
134
|
const toolState = new Map();
|
|
115
|
-
|
|
135
|
+
if (this.options.api === 'responses') {
|
|
136
|
+
yield* streamResponsesObject(this.client, req);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const stream = await createChatCompletion(this.client, req, true, this.getLogger());
|
|
116
140
|
for await (const chunk of stream) {
|
|
117
141
|
req.signal.throwIfAborted();
|
|
118
142
|
if (chunk.usage) {
|
|
@@ -158,11 +182,11 @@ class OpenAiModelProvider extends BaseModelProvider {
|
|
|
158
182
|
}
|
|
159
183
|
}
|
|
160
184
|
function toClientOptions(options) {
|
|
161
|
-
const { client: _client, harnessLogger: _harnessLogger, telemetry: _telemetry, harnessTimeoutMs: _harnessTimeoutMs, ...clientOptions } = options;
|
|
185
|
+
const { api: _api, client: _client, harnessLogger: _harnessLogger, telemetry: _telemetry, harnessTimeoutMs: _harnessTimeoutMs, ...clientOptions } = options;
|
|
162
186
|
return clientOptions;
|
|
163
187
|
}
|
|
164
|
-
function
|
|
165
|
-
const toolCalls =
|
|
188
|
+
function mapChatTextResponse(response, req) {
|
|
189
|
+
const toolCalls = extractChatToolCalls(response, req, 'text');
|
|
166
190
|
return {
|
|
167
191
|
content: response.choices[0]?.message?.content ?? '',
|
|
168
192
|
...(toolCalls ? { toolCalls } : {}),
|
|
@@ -171,7 +195,7 @@ function mapTextResponse(response, req) {
|
|
|
171
195
|
raw: response
|
|
172
196
|
};
|
|
173
197
|
}
|
|
174
|
-
function
|
|
198
|
+
function extractChatToolCalls(response, req, method) {
|
|
175
199
|
const toolCalls = response.choices[0]?.message?.tool_calls;
|
|
176
200
|
if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
|
|
177
201
|
return undefined;
|
|
@@ -184,13 +208,14 @@ function extractToolCalls(response, req, method) {
|
|
|
184
208
|
arguments: parseToolArgs(call.function.arguments, req, method)
|
|
185
209
|
}));
|
|
186
210
|
}
|
|
187
|
-
async function createChatCompletion(client, req, stream) {
|
|
211
|
+
async function createChatCompletion(client, req, stream, logger) {
|
|
188
212
|
const messages = toOpenAiMessages(req.messages);
|
|
189
213
|
const providerOptions = {
|
|
190
214
|
...(req.defaults?.providerOptions ?? {}),
|
|
191
215
|
...(req.call?.providerOptions ?? {})
|
|
192
216
|
};
|
|
193
217
|
const { requestOptions, ...bodyOptions } = providerOptions;
|
|
218
|
+
const normalizedBodyOptions = omitUnsupportedChatCompletionOptions(bodyOptions, req, logger);
|
|
194
219
|
return client.chat.completions.create({
|
|
195
220
|
model: req.model,
|
|
196
221
|
messages,
|
|
@@ -204,9 +229,58 @@ async function createChatCompletion(client, req, stream) {
|
|
|
204
229
|
stop: req.call?.stopSequences ?? req.defaults?.stopSequences,
|
|
205
230
|
...(req.tools && (req.call?.parallelToolCalls ?? req.defaults?.parallelToolCalls) !== undefined ? { parallel_tool_calls: req.call?.parallelToolCalls ?? req.defaults?.parallelToolCalls } : {}),
|
|
206
231
|
response_format: toResponseFormat(req),
|
|
232
|
+
...normalizedBodyOptions
|
|
233
|
+
}, { ...requestOptions, signal: req.signal });
|
|
234
|
+
}
|
|
235
|
+
async function createResponse(client, req, stream) {
|
|
236
|
+
if (!client.responses?.create) {
|
|
237
|
+
throw new ModelError('OpenAI client does not expose the Responses API.', {
|
|
238
|
+
provider: 'openai',
|
|
239
|
+
model: req.model,
|
|
240
|
+
method: stream ? ('schema' in req ? 'objectStream' : 'textStream') : ('schema' in req ? 'object' : 'text'),
|
|
241
|
+
reason: 'unstructured_response',
|
|
242
|
+
providerBody: { api: 'responses' }
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const providerOptions = {
|
|
246
|
+
...(req.defaults?.providerOptions ?? {}),
|
|
247
|
+
...(req.call?.providerOptions ?? {})
|
|
248
|
+
};
|
|
249
|
+
const { requestOptions, ...bodyOptions } = toResponsesProviderOptions(providerOptions);
|
|
250
|
+
return client.responses.create({
|
|
251
|
+
model: req.model,
|
|
252
|
+
input: toResponsesInput(req.messages),
|
|
253
|
+
stream,
|
|
254
|
+
...(stream ? { stream_options: { include_usage: true } } : {}),
|
|
255
|
+
tools: toResponsesTools(req.tools),
|
|
256
|
+
temperature: req.call?.temperature ?? req.defaults?.temperature,
|
|
257
|
+
max_output_tokens: req.call?.maxTokens ?? req.defaults?.maxTokens,
|
|
258
|
+
top_p: req.call?.topP ?? req.defaults?.topP,
|
|
259
|
+
parallel_tool_calls: req.tools && (req.call?.parallelToolCalls ?? req.defaults?.parallelToolCalls) !== undefined ? req.call?.parallelToolCalls ?? req.defaults?.parallelToolCalls : undefined,
|
|
260
|
+
text: toResponsesTextFormat(req),
|
|
207
261
|
...bodyOptions
|
|
208
262
|
}, { ...requestOptions, signal: req.signal });
|
|
209
263
|
}
|
|
264
|
+
function omitUnsupportedChatCompletionOptions(bodyOptions, req, logger) {
|
|
265
|
+
if (!req.tools || req.tools.length === 0 || !('reasoning_effort' in bodyOptions))
|
|
266
|
+
return bodyOptions;
|
|
267
|
+
const { reasoning_effort: _reasoningEffort, ...rest } = bodyOptions;
|
|
268
|
+
logger?.warn('OpenAI reasoning_effort dropped for chat completions with tools.', {
|
|
269
|
+
provider: 'openai',
|
|
270
|
+
model: req.model,
|
|
271
|
+
api: 'chat_completions',
|
|
272
|
+
reason: 'reasoning_effort_not_supported_with_tools',
|
|
273
|
+
recommendation: "Use openai({ api: 'responses' }) for reasoning models that require tools with reasoning effort."
|
|
274
|
+
});
|
|
275
|
+
return rest;
|
|
276
|
+
}
|
|
277
|
+
function toResponsesProviderOptions(providerOptions) {
|
|
278
|
+
const { reasoning_effort: reasoningEffort, reasoning, ...rest } = providerOptions;
|
|
279
|
+
if (reasoningEffort !== undefined && reasoning === undefined) {
|
|
280
|
+
return { ...rest, reasoning: { effort: reasoningEffort } };
|
|
281
|
+
}
|
|
282
|
+
return { ...rest, ...(reasoning !== undefined ? { reasoning } : {}) };
|
|
283
|
+
}
|
|
210
284
|
function toResponseFormat(req) {
|
|
211
285
|
if (!('schema' in req))
|
|
212
286
|
return undefined;
|
|
@@ -219,6 +293,18 @@ function toResponseFormat(req) {
|
|
|
219
293
|
}
|
|
220
294
|
};
|
|
221
295
|
}
|
|
296
|
+
function toResponsesTextFormat(req) {
|
|
297
|
+
if (!('schema' in req))
|
|
298
|
+
return undefined;
|
|
299
|
+
return {
|
|
300
|
+
format: {
|
|
301
|
+
type: 'json_schema',
|
|
302
|
+
name: 'harness_response',
|
|
303
|
+
strict: false,
|
|
304
|
+
schema: req.schema
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
222
308
|
function toOpenAiMessages(messages) {
|
|
223
309
|
return messages.map((message) => {
|
|
224
310
|
if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
|
|
@@ -272,6 +358,67 @@ function toOpenAiMessages(messages) {
|
|
|
272
358
|
};
|
|
273
359
|
});
|
|
274
360
|
}
|
|
361
|
+
function toResponsesInput(messages) {
|
|
362
|
+
const input = [];
|
|
363
|
+
for (const message of messages) {
|
|
364
|
+
if (message.role === 'tool') {
|
|
365
|
+
input.push({
|
|
366
|
+
type: 'function_call_output',
|
|
367
|
+
call_id: message.toolCallId,
|
|
368
|
+
output: message.content
|
|
369
|
+
});
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
|
|
373
|
+
if (typeof message.content === 'string' && message.content.length > 0) {
|
|
374
|
+
input.push({
|
|
375
|
+
type: 'message',
|
|
376
|
+
role: 'assistant',
|
|
377
|
+
content: message.content
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
for (const call of message.toolCalls) {
|
|
381
|
+
input.push({
|
|
382
|
+
type: 'function_call',
|
|
383
|
+
id: call.id,
|
|
384
|
+
call_id: call.id,
|
|
385
|
+
name: call.name,
|
|
386
|
+
arguments: JSON.stringify(call.arguments)
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
input.push({
|
|
392
|
+
type: 'message',
|
|
393
|
+
role: message.role,
|
|
394
|
+
content: toResponsesMessageContent(message)
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return input;
|
|
398
|
+
}
|
|
399
|
+
function toResponsesMessageContent(message) {
|
|
400
|
+
if (typeof message.content === 'string') {
|
|
401
|
+
return message.content;
|
|
402
|
+
}
|
|
403
|
+
return message.content.map((part) => {
|
|
404
|
+
if (part.kind === 'text') {
|
|
405
|
+
return { type: 'input_text', text: part.text };
|
|
406
|
+
}
|
|
407
|
+
if (part.kind === 'image') {
|
|
408
|
+
return {
|
|
409
|
+
type: 'input_image',
|
|
410
|
+
image_url: `data:${part.mimeType};base64,${part.dataBase64}`
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
if (part.kind === 'image_url') {
|
|
414
|
+
return {
|
|
415
|
+
type: 'input_image',
|
|
416
|
+
image_url: part.url
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
return { type: 'input_text', text: `[unsupported ${part.kind} content omitted]` };
|
|
420
|
+
});
|
|
421
|
+
}
|
|
275
422
|
function toTools(tools) {
|
|
276
423
|
if (!tools || tools.length === 0) {
|
|
277
424
|
return undefined;
|
|
@@ -285,6 +432,159 @@ function toTools(tools) {
|
|
|
285
432
|
}
|
|
286
433
|
}));
|
|
287
434
|
}
|
|
435
|
+
function toResponsesTools(tools) {
|
|
436
|
+
if (!tools || tools.length === 0) {
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
return tools.map((tool) => ({
|
|
440
|
+
type: 'function',
|
|
441
|
+
name: tool.name,
|
|
442
|
+
description: tool.description,
|
|
443
|
+
parameters: tool.parameters,
|
|
444
|
+
strict: false
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
function mapResponsesTextResponse(response, req) {
|
|
448
|
+
const toolCalls = extractResponsesToolCalls(response, req, 'text');
|
|
449
|
+
return {
|
|
450
|
+
content: extractResponsesText(response),
|
|
451
|
+
...(toolCalls ? { toolCalls } : {}),
|
|
452
|
+
usage: toResponsesUsage(response.usage),
|
|
453
|
+
finishReason: toResponsesFinishReason(response),
|
|
454
|
+
raw: response
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
async function* streamResponsesText(client, req) {
|
|
458
|
+
const stream = await createResponse(client, req, true);
|
|
459
|
+
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
460
|
+
let finishReason = 'stop';
|
|
461
|
+
const toolState = new Map();
|
|
462
|
+
for await (const event of stream) {
|
|
463
|
+
req.signal.throwIfAborted();
|
|
464
|
+
if (event.type === 'response.output_text.delta') {
|
|
465
|
+
yield { kind: 'delta', text: event.delta };
|
|
466
|
+
}
|
|
467
|
+
else if (event.type === 'response.output_item.added' || event.type === 'response.output_item.done') {
|
|
468
|
+
accumulateResponsesToolCallItem(toolState, event);
|
|
469
|
+
}
|
|
470
|
+
else if (event.type === 'response.function_call_arguments.delta') {
|
|
471
|
+
accumulateResponsesToolCallDelta(toolState, event);
|
|
472
|
+
}
|
|
473
|
+
else if (event.type === 'response.function_call_arguments.done') {
|
|
474
|
+
accumulateResponsesToolCallDone(toolState, event);
|
|
475
|
+
}
|
|
476
|
+
else if (event.type === 'response.completed') {
|
|
477
|
+
usage = toResponsesUsage(event.response?.usage);
|
|
478
|
+
finishReason = toResponsesFinishReason(event.response);
|
|
479
|
+
}
|
|
480
|
+
else if (event.type === 'response.failed' || event.type === 'response.incomplete') {
|
|
481
|
+
finishReason = 'error';
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
for (const call of finalizeResponsesStreamToolCalls(toolState, req, 'textStream')) {
|
|
485
|
+
yield { kind: 'tool_call', call };
|
|
486
|
+
}
|
|
487
|
+
yield { kind: 'finish', usage, finishReason };
|
|
488
|
+
}
|
|
489
|
+
async function* streamResponsesObject(client, req) {
|
|
490
|
+
const stream = await createResponse(client, req, true);
|
|
491
|
+
let partial = '';
|
|
492
|
+
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
493
|
+
let finishReason = 'stop';
|
|
494
|
+
const toolState = new Map();
|
|
495
|
+
for await (const event of stream) {
|
|
496
|
+
req.signal.throwIfAborted();
|
|
497
|
+
if (event.type === 'response.output_text.delta') {
|
|
498
|
+
partial += event.delta;
|
|
499
|
+
yield { kind: 'partial', partial: safePartialJson(partial) };
|
|
500
|
+
}
|
|
501
|
+
else if (event.type === 'response.output_item.added' || event.type === 'response.output_item.done') {
|
|
502
|
+
accumulateResponsesToolCallItem(toolState, event);
|
|
503
|
+
}
|
|
504
|
+
else if (event.type === 'response.function_call_arguments.delta') {
|
|
505
|
+
accumulateResponsesToolCallDelta(toolState, event);
|
|
506
|
+
}
|
|
507
|
+
else if (event.type === 'response.function_call_arguments.done') {
|
|
508
|
+
accumulateResponsesToolCallDone(toolState, event);
|
|
509
|
+
}
|
|
510
|
+
else if (event.type === 'response.completed') {
|
|
511
|
+
usage = toResponsesUsage(event.response?.usage);
|
|
512
|
+
finishReason = toResponsesFinishReason(event.response);
|
|
513
|
+
}
|
|
514
|
+
else if (event.type === 'response.failed' || event.type === 'response.incomplete') {
|
|
515
|
+
finishReason = 'error';
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
for (const call of finalizeResponsesStreamToolCalls(toolState, req, 'objectStream')) {
|
|
519
|
+
yield { kind: 'tool_call', call };
|
|
520
|
+
}
|
|
521
|
+
const object = parseJson(partial || '{}', req, 'objectStream');
|
|
522
|
+
yield { kind: 'finish', object, usage, finishReason };
|
|
523
|
+
}
|
|
524
|
+
function extractResponsesText(response) {
|
|
525
|
+
if (typeof response.output_text === 'string')
|
|
526
|
+
return response.output_text;
|
|
527
|
+
const parts = [];
|
|
528
|
+
for (const item of response.output ?? []) {
|
|
529
|
+
if (item?.type !== 'message')
|
|
530
|
+
continue;
|
|
531
|
+
for (const content of item.content ?? []) {
|
|
532
|
+
if (content?.type === 'output_text' && typeof content.text === 'string') {
|
|
533
|
+
parts.push(content.text);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return parts.join('');
|
|
538
|
+
}
|
|
539
|
+
function extractResponsesToolCalls(response, req, method) {
|
|
540
|
+
const toolCalls = (response.output ?? []).filter((item) => item?.type === 'function_call' && item.call_id && item.name);
|
|
541
|
+
if (toolCalls.length === 0)
|
|
542
|
+
return undefined;
|
|
543
|
+
return toolCalls.map((call) => ({
|
|
544
|
+
id: String(call.call_id),
|
|
545
|
+
name: String(call.name),
|
|
546
|
+
arguments: parseToolArgs(call.arguments, req, method)
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
function accumulateResponsesToolCallItem(state, event) {
|
|
550
|
+
if (event.item?.type !== 'function_call')
|
|
551
|
+
return;
|
|
552
|
+
const index = typeof event.output_index === 'number' ? event.output_index : 0;
|
|
553
|
+
const existing = state.get(index) ?? { args: '' };
|
|
554
|
+
if (event.item.call_id)
|
|
555
|
+
existing.id = String(event.item.call_id);
|
|
556
|
+
if (event.item.name)
|
|
557
|
+
existing.name = String(event.item.name);
|
|
558
|
+
if (typeof event.item.arguments === 'string')
|
|
559
|
+
existing.args = event.item.arguments;
|
|
560
|
+
state.set(index, existing);
|
|
561
|
+
}
|
|
562
|
+
function accumulateResponsesToolCallDelta(state, event) {
|
|
563
|
+
const index = typeof event.output_index === 'number' ? event.output_index : 0;
|
|
564
|
+
const existing = state.get(index) ?? { args: '' };
|
|
565
|
+
if (typeof event.delta === 'string')
|
|
566
|
+
existing.args += event.delta;
|
|
567
|
+
state.set(index, existing);
|
|
568
|
+
}
|
|
569
|
+
function accumulateResponsesToolCallDone(state, event) {
|
|
570
|
+
const index = typeof event.output_index === 'number' ? event.output_index : 0;
|
|
571
|
+
const existing = state.get(index) ?? { args: '' };
|
|
572
|
+
existing.id ??= String(event.item_id);
|
|
573
|
+
existing.name = String(event.name);
|
|
574
|
+
if (typeof event.arguments === 'string')
|
|
575
|
+
existing.args = event.arguments;
|
|
576
|
+
state.set(index, existing);
|
|
577
|
+
}
|
|
578
|
+
function finalizeResponsesStreamToolCalls(state, req, method) {
|
|
579
|
+
return [...state.entries()]
|
|
580
|
+
.sort((a, b) => a[0] - b[0])
|
|
581
|
+
.filter(([, call]) => call.id && call.name)
|
|
582
|
+
.map(([, call]) => ({
|
|
583
|
+
id: call.id,
|
|
584
|
+
name: call.name,
|
|
585
|
+
arguments: parseToolArgs(call.args || undefined, req, method)
|
|
586
|
+
}));
|
|
587
|
+
}
|
|
288
588
|
function accumulateToolCallDeltas(state, deltas) {
|
|
289
589
|
for (const delta of deltas) {
|
|
290
590
|
const index = typeof delta?.index === 'number' ? delta.index : 0;
|
|
@@ -352,6 +652,9 @@ function toUsage(inputTokens, outputTokens) {
|
|
|
352
652
|
totalTokens: input + output
|
|
353
653
|
};
|
|
354
654
|
}
|
|
655
|
+
function toResponsesUsage(usage) {
|
|
656
|
+
return toUsage(usage?.input_tokens, usage?.output_tokens);
|
|
657
|
+
}
|
|
355
658
|
function toFinishReason(value) {
|
|
356
659
|
switch (value) {
|
|
357
660
|
case 'stop':
|
|
@@ -363,3 +666,17 @@ function toFinishReason(value) {
|
|
|
363
666
|
return 'error';
|
|
364
667
|
}
|
|
365
668
|
}
|
|
669
|
+
function toResponsesFinishReason(response) {
|
|
670
|
+
if (!response)
|
|
671
|
+
return 'error';
|
|
672
|
+
if ((response.output ?? []).some((item) => item?.type === 'function_call'))
|
|
673
|
+
return 'tool_calls';
|
|
674
|
+
switch (response.status) {
|
|
675
|
+
case 'completed':
|
|
676
|
+
return 'stop';
|
|
677
|
+
case 'incomplete':
|
|
678
|
+
return 'length';
|
|
679
|
+
default:
|
|
680
|
+
return response.error ? 'error' : 'stop';
|
|
681
|
+
}
|
|
682
|
+
}
|