@purista/harness-openai 1.2.5 → 1.5.0
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 +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +167 -94
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -43,6 +43,14 @@ the adapter drops `reasoning_effort` and emits a warning instead of sending a
|
|
|
43
43
|
request that OpenAI rejects. Use `api: 'responses'` when you need reasoning
|
|
44
44
|
effort and tool calls together.
|
|
45
45
|
|
|
46
|
+
On the Responses API, tool-call responses carry the turn's raw output items
|
|
47
|
+
(including reasoning items) as `providerItems`. The harness agent loop passes
|
|
48
|
+
them back on the follow-up round and the adapter echoes them verbatim, as
|
|
49
|
+
OpenAI recommends for reasoning models with manually managed conversation
|
|
50
|
+
state. For stateless requests (`store: false`), additionally set
|
|
51
|
+
`providerOptions: { store: false, include: ['reasoning.encrypted_content'] }`
|
|
52
|
+
so the encrypted reasoning content rides along in the replayed items.
|
|
53
|
+
|
|
46
54
|
## Package Format
|
|
47
55
|
|
|
48
56
|
This package is ESM-only and ships compiled JavaScript plus TypeScript
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseModelProvider, ModelError } from '@purista/harness';
|
|
1
|
+
import { BaseModelProvider, ModelError, accumulateStreamToolCallDeltas, createStreamToolCallState, finalizeStreamToolCalls, malformedResponseError, parseProviderJson, safePartialJson, sanitizeProviderMessage, toTokenUsage } from '@purista/harness';
|
|
2
2
|
import OpenAI, {} from 'openai';
|
|
3
3
|
/**
|
|
4
4
|
* Creates an OpenAI-backed harness `ModelProvider`.
|
|
@@ -32,6 +32,7 @@ import OpenAI, {} from 'openai';
|
|
|
32
32
|
* summarize: {
|
|
33
33
|
* input: z.string(),
|
|
34
34
|
* output: z.string(),
|
|
35
|
+
* delegation: { agents: ['assistant'] },
|
|
35
36
|
* handler: (ctx) => ctx.agents.assistant(ctx.input)
|
|
36
37
|
* }
|
|
37
38
|
* })
|
|
@@ -62,6 +63,7 @@ class OpenAiModelProvider extends BaseModelProvider {
|
|
|
62
63
|
req.signal.throwIfAborted();
|
|
63
64
|
if (this.options.api === 'responses') {
|
|
64
65
|
const response = await createResponse(this.client, req, false);
|
|
66
|
+
throwIfResponsesFailure(response, req, 'text');
|
|
65
67
|
return mapResponsesTextResponse(response, req);
|
|
66
68
|
}
|
|
67
69
|
const response = await createChatCompletion(this.client, req, false, this.getLogger());
|
|
@@ -76,12 +78,13 @@ class OpenAiModelProvider extends BaseModelProvider {
|
|
|
76
78
|
const stream = await createChatCompletion(this.client, req, true, this.getLogger());
|
|
77
79
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
78
80
|
let finishReason = 'stop';
|
|
79
|
-
|
|
81
|
+
let providerFinishReason;
|
|
82
|
+
const toolState = createStreamToolCallState();
|
|
80
83
|
for await (const chunk of stream) {
|
|
81
84
|
req.signal.throwIfAborted();
|
|
82
85
|
// The usage chunk arrives with an empty choices array, so read it first.
|
|
83
86
|
if (chunk.usage) {
|
|
84
|
-
usage =
|
|
87
|
+
usage = toTokenUsage(chunk.usage.prompt_tokens, chunk.usage.completion_tokens);
|
|
85
88
|
}
|
|
86
89
|
const choice = chunk.choices?.[0];
|
|
87
90
|
if (!choice)
|
|
@@ -90,28 +93,33 @@ class OpenAiModelProvider extends BaseModelProvider {
|
|
|
90
93
|
yield { kind: 'delta', text: choice.delta.content };
|
|
91
94
|
}
|
|
92
95
|
if (choice.delta?.tool_calls) {
|
|
93
|
-
|
|
96
|
+
accumulateStreamToolCallDeltas(toolState, choice.delta.tool_calls);
|
|
94
97
|
}
|
|
95
98
|
if (choice.finish_reason) {
|
|
96
|
-
|
|
99
|
+
providerFinishReason = choice.finish_reason;
|
|
100
|
+
finishReason = toFinishReason(providerFinishReason);
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
|
-
for (const call of finalizeStreamToolCalls(toolState, req, 'textStream')) {
|
|
103
|
+
for (const call of finalizeStreamToolCalls(toolState, callContext(req, 'textStream'), MALFORMED_TOOL_ARGS_MESSAGE)) {
|
|
100
104
|
yield { kind: 'tool_call', call };
|
|
101
105
|
}
|
|
102
|
-
yield { kind: 'finish', usage, finishReason };
|
|
106
|
+
yield { kind: 'finish', usage, finishReason, outcome: toOutcome(finishReason, providerFinishReason) };
|
|
103
107
|
}
|
|
104
108
|
async doObject(req) {
|
|
105
109
|
req.signal.throwIfAborted();
|
|
106
110
|
if (this.options.api === 'responses') {
|
|
107
111
|
const response = await createResponse(this.client, req, false);
|
|
112
|
+
throwIfResponsesFailure(response, req, 'object');
|
|
108
113
|
const content = extractResponsesText(response);
|
|
109
114
|
const toolCalls = extractResponsesToolCalls(response, req, 'object');
|
|
115
|
+
const providerItems = toResponsesProviderItems(response.output, toolCalls);
|
|
110
116
|
return {
|
|
111
117
|
object: parseJson(content || '{}', req, 'object'),
|
|
112
118
|
...(toolCalls ? { toolCalls } : {}),
|
|
119
|
+
...(providerItems ? { providerItems } : {}),
|
|
113
120
|
usage: toResponsesUsage(response.usage),
|
|
114
121
|
finishReason: toResponsesFinishReason(response),
|
|
122
|
+
outcome: toResponsesOutcome(response),
|
|
115
123
|
raw: response
|
|
116
124
|
};
|
|
117
125
|
}
|
|
@@ -121,8 +129,9 @@ class OpenAiModelProvider extends BaseModelProvider {
|
|
|
121
129
|
return {
|
|
122
130
|
object: parseJson(textContent, req, 'object'),
|
|
123
131
|
...(toolCalls ? { toolCalls } : {}),
|
|
124
|
-
usage:
|
|
132
|
+
usage: toTokenUsage(response.usage?.prompt_tokens, response.usage?.completion_tokens),
|
|
125
133
|
finishReason: toFinishReason(response.choices[0]?.finish_reason),
|
|
134
|
+
outcome: toOutcome(toFinishReason(response.choices[0]?.finish_reason), response.choices[0]?.finish_reason),
|
|
126
135
|
raw: response
|
|
127
136
|
};
|
|
128
137
|
}
|
|
@@ -131,7 +140,8 @@ class OpenAiModelProvider extends BaseModelProvider {
|
|
|
131
140
|
let partial = '';
|
|
132
141
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
133
142
|
let finishReason = 'stop';
|
|
134
|
-
|
|
143
|
+
let providerFinishReason;
|
|
144
|
+
const toolState = createStreamToolCallState();
|
|
135
145
|
if (this.options.api === 'responses') {
|
|
136
146
|
yield* streamResponsesObject(this.client, req);
|
|
137
147
|
return;
|
|
@@ -140,7 +150,7 @@ class OpenAiModelProvider extends BaseModelProvider {
|
|
|
140
150
|
for await (const chunk of stream) {
|
|
141
151
|
req.signal.throwIfAborted();
|
|
142
152
|
if (chunk.usage) {
|
|
143
|
-
usage =
|
|
153
|
+
usage = toTokenUsage(chunk.usage.prompt_tokens, chunk.usage.completion_tokens);
|
|
144
154
|
}
|
|
145
155
|
const choice = chunk.choices?.[0];
|
|
146
156
|
if (!choice)
|
|
@@ -150,17 +160,18 @@ class OpenAiModelProvider extends BaseModelProvider {
|
|
|
150
160
|
yield { kind: 'partial', partial: safePartialJson(partial) };
|
|
151
161
|
}
|
|
152
162
|
if (choice.delta?.tool_calls) {
|
|
153
|
-
|
|
163
|
+
accumulateStreamToolCallDeltas(toolState, choice.delta.tool_calls);
|
|
154
164
|
}
|
|
155
165
|
if (choice.finish_reason) {
|
|
156
|
-
|
|
166
|
+
providerFinishReason = choice.finish_reason;
|
|
167
|
+
finishReason = toFinishReason(providerFinishReason);
|
|
157
168
|
}
|
|
158
169
|
}
|
|
159
|
-
for (const call of finalizeStreamToolCalls(toolState, req, 'objectStream')) {
|
|
170
|
+
for (const call of finalizeStreamToolCalls(toolState, callContext(req, 'objectStream'), MALFORMED_TOOL_ARGS_MESSAGE)) {
|
|
160
171
|
yield { kind: 'tool_call', call };
|
|
161
172
|
}
|
|
162
173
|
const object = parseJson(partial || '{}', req, 'objectStream');
|
|
163
|
-
yield { kind: 'finish', object, usage, finishReason };
|
|
174
|
+
yield { kind: 'finish', object, usage, finishReason, outcome: toOutcome(finishReason, providerFinishReason) };
|
|
164
175
|
}
|
|
165
176
|
async doEmbed(req) {
|
|
166
177
|
req.signal.throwIfAborted();
|
|
@@ -176,22 +187,25 @@ class OpenAiModelProvider extends BaseModelProvider {
|
|
|
176
187
|
}, { ...requestOptions, signal: req.signal });
|
|
177
188
|
return {
|
|
178
189
|
embeddings: response.data.map((item) => ({ index: item.index, vector: item.embedding })),
|
|
179
|
-
usage:
|
|
190
|
+
usage: toTokenUsage(response.usage?.prompt_tokens, 0),
|
|
180
191
|
raw: response
|
|
181
192
|
};
|
|
182
193
|
}
|
|
183
194
|
}
|
|
184
195
|
function toClientOptions(options) {
|
|
185
196
|
const { api: _api, client: _client, harnessLogger: _harnessLogger, telemetry: _telemetry, harnessTimeoutMs: _harnessTimeoutMs, ...clientOptions } = options;
|
|
186
|
-
return clientOptions;
|
|
197
|
+
return { maxRetries: 0, ...clientOptions };
|
|
187
198
|
}
|
|
188
199
|
function mapChatTextResponse(response, req) {
|
|
189
200
|
const toolCalls = extractChatToolCalls(response, req, 'text');
|
|
201
|
+
const providerFinishReason = response.choices[0]?.finish_reason;
|
|
202
|
+
const finishReason = toFinishReason(providerFinishReason);
|
|
190
203
|
return {
|
|
191
204
|
content: response.choices[0]?.message?.content ?? '',
|
|
192
205
|
...(toolCalls ? { toolCalls } : {}),
|
|
193
|
-
usage:
|
|
194
|
-
finishReason
|
|
206
|
+
usage: toTokenUsage(response.usage?.prompt_tokens, response.usage?.completion_tokens),
|
|
207
|
+
finishReason,
|
|
208
|
+
outcome: toOutcome(finishReason, providerFinishReason),
|
|
195
209
|
raw: response
|
|
196
210
|
};
|
|
197
211
|
}
|
|
@@ -368,6 +382,13 @@ function toResponsesInput(messages) {
|
|
|
368
382
|
});
|
|
369
383
|
continue;
|
|
370
384
|
}
|
|
385
|
+
if (message.role === 'assistant' && message.providerItems?.providerId === 'openai' && message.providerItems.items.length > 0) {
|
|
386
|
+
// Echo the captured turn (reasoning, message, and function_call items)
|
|
387
|
+
// verbatim, as the Responses API expects for manually managed state.
|
|
388
|
+
// Foreign provider items fall through to provider-neutral reconstruction.
|
|
389
|
+
input.push(...message.providerItems.items);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
371
392
|
if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
|
|
372
393
|
if (typeof message.content === 'string' && message.content.length > 0) {
|
|
373
394
|
input.push({
|
|
@@ -377,9 +398,11 @@ function toResponsesInput(messages) {
|
|
|
377
398
|
});
|
|
378
399
|
}
|
|
379
400
|
for (const call of message.toolCalls) {
|
|
401
|
+
// The Responses API only accepts an `fc_…` item id on `function_call`
|
|
402
|
+
// input items; the harness tool-call id is the `call_…` value, so the
|
|
403
|
+
// optional item id is omitted, mirroring `function_call_output`.
|
|
380
404
|
input.push({
|
|
381
405
|
type: 'function_call',
|
|
382
|
-
id: call.id,
|
|
383
406
|
call_id: call.id,
|
|
384
407
|
name: call.name,
|
|
385
408
|
arguments: JSON.stringify(call.arguments)
|
|
@@ -445,18 +468,37 @@ function toResponsesTools(tools) {
|
|
|
445
468
|
}
|
|
446
469
|
function mapResponsesTextResponse(response, req) {
|
|
447
470
|
const toolCalls = extractResponsesToolCalls(response, req, 'text');
|
|
471
|
+
const providerItems = toResponsesProviderItems(response.output, toolCalls);
|
|
448
472
|
return {
|
|
449
473
|
content: extractResponsesText(response),
|
|
450
474
|
...(toolCalls ? { toolCalls } : {}),
|
|
475
|
+
...(providerItems ? { providerItems } : {}),
|
|
451
476
|
usage: toResponsesUsage(response.usage),
|
|
452
477
|
finishReason: toResponsesFinishReason(response),
|
|
478
|
+
outcome: toResponsesOutcome(response),
|
|
453
479
|
raw: response
|
|
454
480
|
};
|
|
455
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Captures the turn's raw Responses output items on tool-call responses so
|
|
484
|
+
* they can be replayed verbatim on the follow-up round. OpenAI requires
|
|
485
|
+
* reasoning items returned with tool calls to be passed back with the tool
|
|
486
|
+
* outputs for reasoning models; echoing `response.output` unchanged is the
|
|
487
|
+
* pattern documented in the Responses migration guide.
|
|
488
|
+
*/
|
|
489
|
+
function toResponsesProviderItems(output, toolCalls) {
|
|
490
|
+
if (!toolCalls || toolCalls.length === 0)
|
|
491
|
+
return undefined;
|
|
492
|
+
if (!Array.isArray(output) || output.length === 0)
|
|
493
|
+
return undefined;
|
|
494
|
+
return { providerId: 'openai', items: output };
|
|
495
|
+
}
|
|
456
496
|
async function* streamResponsesText(client, req) {
|
|
457
497
|
const stream = await createResponse(client, req, true);
|
|
458
498
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
459
499
|
let finishReason = 'stop';
|
|
500
|
+
let outcome = toOutcome('stop');
|
|
501
|
+
let completedOutput;
|
|
460
502
|
const toolState = new Map();
|
|
461
503
|
for await (const event of stream) {
|
|
462
504
|
req.signal.throwIfAborted();
|
|
@@ -475,21 +517,34 @@ async function* streamResponsesText(client, req) {
|
|
|
475
517
|
else if (event.type === 'response.completed') {
|
|
476
518
|
usage = toResponsesUsage(event.response?.usage);
|
|
477
519
|
finishReason = toResponsesFinishReason(event.response);
|
|
520
|
+
outcome = toResponsesOutcome(event.response);
|
|
521
|
+
completedOutput = event.response?.output;
|
|
522
|
+
}
|
|
523
|
+
else if (event.type === 'response.failed') {
|
|
524
|
+
// A genuine provider failure must surface as an error so base retry and
|
|
525
|
+
// normalization apply, matching the chat-completions path.
|
|
526
|
+
throw responsesFailureError(event.response, req, 'textStream');
|
|
478
527
|
}
|
|
479
|
-
else if (event.type === 'response.
|
|
480
|
-
|
|
528
|
+
else if (event.type === 'response.incomplete') {
|
|
529
|
+
usage = toResponsesUsage(event.response?.usage);
|
|
530
|
+
finishReason = toResponsesFinishReason(event.response);
|
|
531
|
+
outcome = toResponsesOutcome(event.response);
|
|
481
532
|
}
|
|
482
533
|
}
|
|
483
|
-
|
|
534
|
+
const toolCalls = finalizeResponsesStreamToolCalls(toolState, req, 'textStream');
|
|
535
|
+
for (const call of toolCalls) {
|
|
484
536
|
yield { kind: 'tool_call', call };
|
|
485
537
|
}
|
|
486
|
-
|
|
538
|
+
const providerItems = toResponsesProviderItems(completedOutput, toolCalls);
|
|
539
|
+
yield { kind: 'finish', usage, finishReason, outcome, ...(providerItems ? { providerItems } : {}) };
|
|
487
540
|
}
|
|
488
541
|
async function* streamResponsesObject(client, req) {
|
|
489
542
|
const stream = await createResponse(client, req, true);
|
|
490
543
|
let partial = '';
|
|
491
544
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
492
545
|
let finishReason = 'stop';
|
|
546
|
+
let outcome = toOutcome('stop');
|
|
547
|
+
let completedOutput;
|
|
493
548
|
const toolState = new Map();
|
|
494
549
|
for await (const event of stream) {
|
|
495
550
|
req.signal.throwIfAborted();
|
|
@@ -509,16 +564,27 @@ async function* streamResponsesObject(client, req) {
|
|
|
509
564
|
else if (event.type === 'response.completed') {
|
|
510
565
|
usage = toResponsesUsage(event.response?.usage);
|
|
511
566
|
finishReason = toResponsesFinishReason(event.response);
|
|
567
|
+
outcome = toResponsesOutcome(event.response);
|
|
568
|
+
completedOutput = event.response?.output;
|
|
512
569
|
}
|
|
513
|
-
else if (event.type === 'response.failed'
|
|
514
|
-
|
|
570
|
+
else if (event.type === 'response.failed') {
|
|
571
|
+
// A genuine provider failure must surface as an error so base retry and
|
|
572
|
+
// normalization apply, matching the chat-completions path.
|
|
573
|
+
throw responsesFailureError(event.response, req, 'objectStream');
|
|
574
|
+
}
|
|
575
|
+
else if (event.type === 'response.incomplete') {
|
|
576
|
+
usage = toResponsesUsage(event.response?.usage);
|
|
577
|
+
finishReason = toResponsesFinishReason(event.response);
|
|
578
|
+
outcome = toResponsesOutcome(event.response);
|
|
515
579
|
}
|
|
516
580
|
}
|
|
517
|
-
|
|
581
|
+
const toolCalls = finalizeResponsesStreamToolCalls(toolState, req, 'objectStream');
|
|
582
|
+
for (const call of toolCalls) {
|
|
518
583
|
yield { kind: 'tool_call', call };
|
|
519
584
|
}
|
|
520
585
|
const object = parseJson(partial || '{}', req, 'objectStream');
|
|
521
|
-
|
|
586
|
+
const providerItems = toResponsesProviderItems(completedOutput, toolCalls);
|
|
587
|
+
yield { kind: 'finish', object, usage, finishReason, outcome, ...(providerItems ? { providerItems } : {}) };
|
|
522
588
|
}
|
|
523
589
|
function extractResponsesText(response) {
|
|
524
590
|
if (typeof response.output_text === 'string')
|
|
@@ -568,8 +634,11 @@ function accumulateResponsesToolCallDelta(state, event) {
|
|
|
568
634
|
function accumulateResponsesToolCallDone(state, event) {
|
|
569
635
|
const index = typeof event.output_index === 'number' ? event.output_index : 0;
|
|
570
636
|
const existing = state.get(index) ?? { args: '' };
|
|
571
|
-
|
|
572
|
-
|
|
637
|
+
// `event.item_id` is the `fc_…` item id, not the `call_…` id required for
|
|
638
|
+
// `function_call_output`, so the call id only ever comes from the
|
|
639
|
+
// `response.output_item.added`/`done` events.
|
|
640
|
+
if (event.name)
|
|
641
|
+
existing.name = String(event.name);
|
|
573
642
|
if (typeof event.arguments === 'string')
|
|
574
643
|
existing.args = event.arguments;
|
|
575
644
|
state.set(index, existing);
|
|
@@ -577,82 +646,58 @@ function accumulateResponsesToolCallDone(state, event) {
|
|
|
577
646
|
function finalizeResponsesStreamToolCalls(state, req, method) {
|
|
578
647
|
return [...state.entries()]
|
|
579
648
|
.sort((a, b) => a[0] - b[0])
|
|
580
|
-
.filter(([, call]) => call.
|
|
581
|
-
.map(([, call]) =>
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (delta?.id)
|
|
592
|
-
existing.id = String(delta.id);
|
|
593
|
-
if (delta?.function?.name)
|
|
594
|
-
existing.name = String(delta.function.name);
|
|
595
|
-
if (typeof delta?.function?.arguments === 'string')
|
|
596
|
-
existing.args += delta.function.arguments;
|
|
597
|
-
state.set(index, existing);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
function finalizeStreamToolCalls(state, req, method) {
|
|
601
|
-
return [...state.entries()]
|
|
602
|
-
.sort((a, b) => a[0] - b[0])
|
|
603
|
-
.filter(([, call]) => call.id && call.name)
|
|
604
|
-
.map(([, call]) => ({
|
|
605
|
-
id: call.id,
|
|
606
|
-
name: call.name,
|
|
607
|
-
arguments: parseToolArgs(call.args || undefined, req, method)
|
|
608
|
-
}));
|
|
649
|
+
.filter(([, call]) => call.name)
|
|
650
|
+
.map(([, call]) => {
|
|
651
|
+
if (!call.id) {
|
|
652
|
+
throw malformedResponseError(callContext(req, method), 'OpenAI streamed a function call without a call_id.', call, undefined);
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
id: call.id,
|
|
656
|
+
name: call.name,
|
|
657
|
+
arguments: parseToolArgs(call.args || undefined, req, method)
|
|
658
|
+
};
|
|
659
|
+
});
|
|
609
660
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
return JSON.parse(argumentsText);
|
|
615
|
-
}
|
|
616
|
-
catch (error) {
|
|
617
|
-
throw malformedResponseError(req, method, 'OpenAI returned malformed tool-call argument JSON.', argumentsText, error);
|
|
618
|
-
}
|
|
661
|
+
const MALFORMED_TOOL_ARGS_MESSAGE = 'OpenAI returned malformed tool-call argument JSON.';
|
|
662
|
+
const MALFORMED_OBJECT_MESSAGE = 'OpenAI returned malformed structured object JSON.';
|
|
663
|
+
function callContext(req, method) {
|
|
664
|
+
return { provider: 'openai', model: req.model, method };
|
|
619
665
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
catch (error) {
|
|
625
|
-
throw malformedResponseError(req, method, 'OpenAI returned malformed structured object JSON.', content, error);
|
|
666
|
+
/** Throws when a non-streaming Responses result reports a provider failure. */
|
|
667
|
+
function throwIfResponsesFailure(response, req, method) {
|
|
668
|
+
if (response?.status === 'failed' || response?.error) {
|
|
669
|
+
throw responsesFailureError(response, req, method);
|
|
626
670
|
}
|
|
627
671
|
}
|
|
628
|
-
|
|
629
|
-
|
|
672
|
+
/**
|
|
673
|
+
* Maps a failed Responses-API result into a `ModelError` so the base
|
|
674
|
+
* provider's retry classification and normalization apply.
|
|
675
|
+
*/
|
|
676
|
+
function responsesFailureError(response, req, method) {
|
|
677
|
+
const providerCode = typeof response?.error?.code === 'string' ? response.error.code : undefined;
|
|
678
|
+
const rawMessage = typeof response?.error?.message === 'string' ? response.error.message : undefined;
|
|
679
|
+
const reason = providerCode === 'rate_limit_exceeded'
|
|
680
|
+
? 'rate_limited'
|
|
681
|
+
: providerCode === 'server_error'
|
|
682
|
+
? 'provider_unavailable'
|
|
683
|
+
: 'http_error';
|
|
684
|
+
return new ModelError('OpenAI reported a failed response.', {
|
|
630
685
|
provider: 'openai',
|
|
631
686
|
model: req.model,
|
|
632
687
|
method,
|
|
633
|
-
reason
|
|
634
|
-
|
|
635
|
-
|
|
688
|
+
reason,
|
|
689
|
+
...(providerCode ? { providerCode } : {}),
|
|
690
|
+
...(rawMessage ? { providerMessage: sanitizeProviderMessage(rawMessage) } : {})
|
|
691
|
+
});
|
|
636
692
|
}
|
|
637
|
-
function
|
|
638
|
-
|
|
639
|
-
return JSON.parse(content);
|
|
640
|
-
}
|
|
641
|
-
catch {
|
|
642
|
-
return { _partial: content };
|
|
643
|
-
}
|
|
693
|
+
function parseToolArgs(argumentsText, req, method) {
|
|
694
|
+
return parseProviderJson(argumentsText || '{}', callContext(req, method), MALFORMED_TOOL_ARGS_MESSAGE);
|
|
644
695
|
}
|
|
645
|
-
function
|
|
646
|
-
|
|
647
|
-
const output = outputTokens ?? 0;
|
|
648
|
-
return {
|
|
649
|
-
inputTokens: input,
|
|
650
|
-
outputTokens: output,
|
|
651
|
-
totalTokens: input + output
|
|
652
|
-
};
|
|
696
|
+
function parseJson(content, req, method) {
|
|
697
|
+
return parseProviderJson(content, callContext(req, method), MALFORMED_OBJECT_MESSAGE);
|
|
653
698
|
}
|
|
654
699
|
function toResponsesUsage(usage) {
|
|
655
|
-
return
|
|
700
|
+
return toTokenUsage(usage?.input_tokens, usage?.output_tokens);
|
|
656
701
|
}
|
|
657
702
|
function toFinishReason(value) {
|
|
658
703
|
switch (value) {
|
|
@@ -661,6 +706,8 @@ function toFinishReason(value) {
|
|
|
661
706
|
case 'tool_calls':
|
|
662
707
|
case 'content_filter':
|
|
663
708
|
return value;
|
|
709
|
+
case 'function_call':
|
|
710
|
+
return 'tool_calls';
|
|
664
711
|
default:
|
|
665
712
|
return 'error';
|
|
666
713
|
}
|
|
@@ -670,6 +717,11 @@ function toResponsesFinishReason(response) {
|
|
|
670
717
|
return 'error';
|
|
671
718
|
if ((response.output ?? []).some((item) => item?.type === 'function_call'))
|
|
672
719
|
return 'tool_calls';
|
|
720
|
+
const incompleteReason = response.incomplete_details?.reason;
|
|
721
|
+
if (incompleteReason === 'max_output_tokens')
|
|
722
|
+
return 'length';
|
|
723
|
+
if (incompleteReason === 'content_filter')
|
|
724
|
+
return 'content_filter';
|
|
673
725
|
switch (response.status) {
|
|
674
726
|
case 'completed':
|
|
675
727
|
return 'stop';
|
|
@@ -679,3 +731,24 @@ function toResponsesFinishReason(response) {
|
|
|
679
731
|
return response.error ? 'error' : 'stop';
|
|
680
732
|
}
|
|
681
733
|
}
|
|
734
|
+
function toOutcome(finishReason, providerFinishReason, details) {
|
|
735
|
+
return {
|
|
736
|
+
finishReason,
|
|
737
|
+
...(typeof providerFinishReason === 'string' ? { providerFinishReason } : {}),
|
|
738
|
+
...(details ? { details } : {})
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function toResponsesOutcome(response) {
|
|
742
|
+
const finishReason = toResponsesFinishReason(response);
|
|
743
|
+
const details = response?.incomplete_details || response?.error
|
|
744
|
+
? {
|
|
745
|
+
...(response.incomplete_details ? { incompleteDetails: response.incomplete_details } : {}),
|
|
746
|
+
...(response.error ? { error: response.error } : {})
|
|
747
|
+
}
|
|
748
|
+
: undefined;
|
|
749
|
+
return {
|
|
750
|
+
finishReason,
|
|
751
|
+
...(typeof response?.status === 'string' ? { providerStatus: response.status, providerFinishReason: response.status } : {}),
|
|
752
|
+
...(details ? { details } : {})
|
|
753
|
+
};
|
|
754
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@purista/harness-openai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "OpenAI model provider adapter for @purista/harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"vitest": "^4.1.8"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@purista/harness": "
|
|
51
|
+
"@purista/harness": "^1.5.0"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=24.15.0"
|