@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 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
@@ -53,6 +53,7 @@ export interface OpenAiFactoryOptions extends ClientOptions {
53
53
  * summarize: {
54
54
  * input: z.string(),
55
55
  * output: z.string(),
56
+ * delegation: { agents: ['assistant'] },
56
57
  * handler: (ctx) => ctx.agents.assistant(ctx.input)
57
58
  * }
58
59
  * })
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
- const toolState = new Map();
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 = toUsage(chunk.usage.prompt_tokens, chunk.usage.completion_tokens);
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
- accumulateToolCallDeltas(toolState, choice.delta.tool_calls);
96
+ accumulateStreamToolCallDeltas(toolState, choice.delta.tool_calls);
94
97
  }
95
98
  if (choice.finish_reason) {
96
- finishReason = toFinishReason(choice.finish_reason);
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: toUsage(response.usage?.prompt_tokens, response.usage?.completion_tokens),
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
- const toolState = new Map();
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 = toUsage(chunk.usage.prompt_tokens, chunk.usage.completion_tokens);
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
- accumulateToolCallDeltas(toolState, choice.delta.tool_calls);
163
+ accumulateStreamToolCallDeltas(toolState, choice.delta.tool_calls);
154
164
  }
155
165
  if (choice.finish_reason) {
156
- finishReason = toFinishReason(choice.finish_reason);
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: toUsage(response.usage?.prompt_tokens, 0),
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: toUsage(response.usage?.prompt_tokens, response.usage?.completion_tokens),
194
- finishReason: toFinishReason(response.choices[0]?.finish_reason),
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.failed' || event.type === 'response.incomplete') {
480
- finishReason = 'error';
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
- for (const call of finalizeResponsesStreamToolCalls(toolState, req, 'textStream')) {
534
+ const toolCalls = finalizeResponsesStreamToolCalls(toolState, req, 'textStream');
535
+ for (const call of toolCalls) {
484
536
  yield { kind: 'tool_call', call };
485
537
  }
486
- yield { kind: 'finish', usage, finishReason };
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' || event.type === 'response.incomplete') {
514
- finishReason = 'error';
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
- for (const call of finalizeResponsesStreamToolCalls(toolState, req, 'objectStream')) {
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
- yield { kind: 'finish', object, usage, finishReason };
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
- existing.id ??= String(event.item_id);
572
- existing.name = String(event.name);
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.id && call.name)
581
- .map(([, call]) => ({
582
- id: call.id,
583
- name: call.name,
584
- arguments: parseToolArgs(call.args || undefined, req, method)
585
- }));
586
- }
587
- function accumulateToolCallDeltas(state, deltas) {
588
- for (const delta of deltas) {
589
- const index = typeof delta?.index === 'number' ? delta.index : 0;
590
- const existing = state.get(index) ?? { args: '' };
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
- function parseToolArgs(argumentsText, req, method) {
611
- if (!argumentsText)
612
- return {};
613
- try {
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
- function parseJson(content, req, method) {
621
- try {
622
- return JSON.parse(content);
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
- function malformedResponseError(req, method, message, body, cause) {
629
- return new ModelError(message, {
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: 'malformed_response',
634
- providerBody: body
635
- }, cause);
688
+ reason,
689
+ ...(providerCode ? { providerCode } : {}),
690
+ ...(rawMessage ? { providerMessage: sanitizeProviderMessage(rawMessage) } : {})
691
+ });
636
692
  }
637
- function safePartialJson(content) {
638
- try {
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 toUsage(inputTokens, outputTokens) {
646
- const input = inputTokens ?? 0;
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 toUsage(usage?.input_tokens, usage?.output_tokens);
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.2.5",
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"