@purista/harness-openai 1.2.5 → 1.2.6
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.js +56 -13
- package/package.json +1 -1
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.js
CHANGED
|
@@ -107,9 +107,11 @@ class OpenAiModelProvider extends BaseModelProvider {
|
|
|
107
107
|
const response = await createResponse(this.client, req, false);
|
|
108
108
|
const content = extractResponsesText(response);
|
|
109
109
|
const toolCalls = extractResponsesToolCalls(response, req, 'object');
|
|
110
|
+
const providerItems = toResponsesProviderItems(response.output, toolCalls);
|
|
110
111
|
return {
|
|
111
112
|
object: parseJson(content || '{}', req, 'object'),
|
|
112
113
|
...(toolCalls ? { toolCalls } : {}),
|
|
114
|
+
...(providerItems ? { providerItems } : {}),
|
|
113
115
|
usage: toResponsesUsage(response.usage),
|
|
114
116
|
finishReason: toResponsesFinishReason(response),
|
|
115
117
|
raw: response
|
|
@@ -368,6 +370,13 @@ function toResponsesInput(messages) {
|
|
|
368
370
|
});
|
|
369
371
|
continue;
|
|
370
372
|
}
|
|
373
|
+
if (message.role === 'assistant' && message.providerItems?.providerId === 'openai' && message.providerItems.items.length > 0) {
|
|
374
|
+
// Echo the captured turn (reasoning, message, and function_call items)
|
|
375
|
+
// verbatim, as the Responses API expects for manually managed state.
|
|
376
|
+
// Foreign provider items fall through to provider-neutral reconstruction.
|
|
377
|
+
input.push(...message.providerItems.items);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
371
380
|
if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
|
|
372
381
|
if (typeof message.content === 'string' && message.content.length > 0) {
|
|
373
382
|
input.push({
|
|
@@ -377,9 +386,11 @@ function toResponsesInput(messages) {
|
|
|
377
386
|
});
|
|
378
387
|
}
|
|
379
388
|
for (const call of message.toolCalls) {
|
|
389
|
+
// The Responses API only accepts an `fc_…` item id on `function_call`
|
|
390
|
+
// input items; the harness tool-call id is the `call_…` value, so the
|
|
391
|
+
// optional item id is omitted, mirroring `function_call_output`.
|
|
380
392
|
input.push({
|
|
381
393
|
type: 'function_call',
|
|
382
|
-
id: call.id,
|
|
383
394
|
call_id: call.id,
|
|
384
395
|
name: call.name,
|
|
385
396
|
arguments: JSON.stringify(call.arguments)
|
|
@@ -445,18 +456,35 @@ function toResponsesTools(tools) {
|
|
|
445
456
|
}
|
|
446
457
|
function mapResponsesTextResponse(response, req) {
|
|
447
458
|
const toolCalls = extractResponsesToolCalls(response, req, 'text');
|
|
459
|
+
const providerItems = toResponsesProviderItems(response.output, toolCalls);
|
|
448
460
|
return {
|
|
449
461
|
content: extractResponsesText(response),
|
|
450
462
|
...(toolCalls ? { toolCalls } : {}),
|
|
463
|
+
...(providerItems ? { providerItems } : {}),
|
|
451
464
|
usage: toResponsesUsage(response.usage),
|
|
452
465
|
finishReason: toResponsesFinishReason(response),
|
|
453
466
|
raw: response
|
|
454
467
|
};
|
|
455
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Captures the turn's raw Responses output items on tool-call responses so
|
|
471
|
+
* they can be replayed verbatim on the follow-up round. OpenAI requires
|
|
472
|
+
* reasoning items returned with tool calls to be passed back with the tool
|
|
473
|
+
* outputs for reasoning models; echoing `response.output` unchanged is the
|
|
474
|
+
* pattern documented in the Responses migration guide.
|
|
475
|
+
*/
|
|
476
|
+
function toResponsesProviderItems(output, toolCalls) {
|
|
477
|
+
if (!toolCalls || toolCalls.length === 0)
|
|
478
|
+
return undefined;
|
|
479
|
+
if (!Array.isArray(output) || output.length === 0)
|
|
480
|
+
return undefined;
|
|
481
|
+
return { providerId: 'openai', items: output };
|
|
482
|
+
}
|
|
456
483
|
async function* streamResponsesText(client, req) {
|
|
457
484
|
const stream = await createResponse(client, req, true);
|
|
458
485
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
459
486
|
let finishReason = 'stop';
|
|
487
|
+
let completedOutput;
|
|
460
488
|
const toolState = new Map();
|
|
461
489
|
for await (const event of stream) {
|
|
462
490
|
req.signal.throwIfAborted();
|
|
@@ -475,21 +503,25 @@ async function* streamResponsesText(client, req) {
|
|
|
475
503
|
else if (event.type === 'response.completed') {
|
|
476
504
|
usage = toResponsesUsage(event.response?.usage);
|
|
477
505
|
finishReason = toResponsesFinishReason(event.response);
|
|
506
|
+
completedOutput = event.response?.output;
|
|
478
507
|
}
|
|
479
508
|
else if (event.type === 'response.failed' || event.type === 'response.incomplete') {
|
|
480
509
|
finishReason = 'error';
|
|
481
510
|
}
|
|
482
511
|
}
|
|
483
|
-
|
|
512
|
+
const toolCalls = finalizeResponsesStreamToolCalls(toolState, req, 'textStream');
|
|
513
|
+
for (const call of toolCalls) {
|
|
484
514
|
yield { kind: 'tool_call', call };
|
|
485
515
|
}
|
|
486
|
-
|
|
516
|
+
const providerItems = toResponsesProviderItems(completedOutput, toolCalls);
|
|
517
|
+
yield { kind: 'finish', usage, finishReason, ...(providerItems ? { providerItems } : {}) };
|
|
487
518
|
}
|
|
488
519
|
async function* streamResponsesObject(client, req) {
|
|
489
520
|
const stream = await createResponse(client, req, true);
|
|
490
521
|
let partial = '';
|
|
491
522
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
492
523
|
let finishReason = 'stop';
|
|
524
|
+
let completedOutput;
|
|
493
525
|
const toolState = new Map();
|
|
494
526
|
for await (const event of stream) {
|
|
495
527
|
req.signal.throwIfAborted();
|
|
@@ -509,16 +541,19 @@ async function* streamResponsesObject(client, req) {
|
|
|
509
541
|
else if (event.type === 'response.completed') {
|
|
510
542
|
usage = toResponsesUsage(event.response?.usage);
|
|
511
543
|
finishReason = toResponsesFinishReason(event.response);
|
|
544
|
+
completedOutput = event.response?.output;
|
|
512
545
|
}
|
|
513
546
|
else if (event.type === 'response.failed' || event.type === 'response.incomplete') {
|
|
514
547
|
finishReason = 'error';
|
|
515
548
|
}
|
|
516
549
|
}
|
|
517
|
-
|
|
550
|
+
const toolCalls = finalizeResponsesStreamToolCalls(toolState, req, 'objectStream');
|
|
551
|
+
for (const call of toolCalls) {
|
|
518
552
|
yield { kind: 'tool_call', call };
|
|
519
553
|
}
|
|
520
554
|
const object = parseJson(partial || '{}', req, 'objectStream');
|
|
521
|
-
|
|
555
|
+
const providerItems = toResponsesProviderItems(completedOutput, toolCalls);
|
|
556
|
+
yield { kind: 'finish', object, usage, finishReason, ...(providerItems ? { providerItems } : {}) };
|
|
522
557
|
}
|
|
523
558
|
function extractResponsesText(response) {
|
|
524
559
|
if (typeof response.output_text === 'string')
|
|
@@ -568,8 +603,11 @@ function accumulateResponsesToolCallDelta(state, event) {
|
|
|
568
603
|
function accumulateResponsesToolCallDone(state, event) {
|
|
569
604
|
const index = typeof event.output_index === 'number' ? event.output_index : 0;
|
|
570
605
|
const existing = state.get(index) ?? { args: '' };
|
|
571
|
-
|
|
572
|
-
|
|
606
|
+
// `event.item_id` is the `fc_…` item id, not the `call_…` id required for
|
|
607
|
+
// `function_call_output`, so the call id only ever comes from the
|
|
608
|
+
// `response.output_item.added`/`done` events.
|
|
609
|
+
if (event.name)
|
|
610
|
+
existing.name = String(event.name);
|
|
573
611
|
if (typeof event.arguments === 'string')
|
|
574
612
|
existing.args = event.arguments;
|
|
575
613
|
state.set(index, existing);
|
|
@@ -577,12 +615,17 @@ function accumulateResponsesToolCallDone(state, event) {
|
|
|
577
615
|
function finalizeResponsesStreamToolCalls(state, req, method) {
|
|
578
616
|
return [...state.entries()]
|
|
579
617
|
.sort((a, b) => a[0] - b[0])
|
|
580
|
-
.filter(([, call]) => call.
|
|
581
|
-
.map(([, call]) =>
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
618
|
+
.filter(([, call]) => call.name)
|
|
619
|
+
.map(([, call]) => {
|
|
620
|
+
if (!call.id) {
|
|
621
|
+
throw malformedResponseError(req, method, 'OpenAI streamed a function call without a call_id.', call, undefined);
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
id: call.id,
|
|
625
|
+
name: call.name,
|
|
626
|
+
arguments: parseToolArgs(call.args || undefined, req, method)
|
|
627
|
+
};
|
|
628
|
+
});
|
|
586
629
|
}
|
|
587
630
|
function accumulateToolCallDeltas(state, deltas) {
|
|
588
631
|
for (const delta of deltas) {
|