@purista/harness-openai 1.2.4 → 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 -14
- 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
|
|
@@ -251,7 +253,6 @@ async function createResponse(client, req, stream) {
|
|
|
251
253
|
model: req.model,
|
|
252
254
|
input: toResponsesInput(req.messages),
|
|
253
255
|
stream,
|
|
254
|
-
...(stream ? { stream_options: { include_usage: true } } : {}),
|
|
255
256
|
tools: toResponsesTools(req.tools),
|
|
256
257
|
temperature: req.call?.temperature ?? req.defaults?.temperature,
|
|
257
258
|
max_output_tokens: req.call?.maxTokens ?? req.defaults?.maxTokens,
|
|
@@ -369,6 +370,13 @@ function toResponsesInput(messages) {
|
|
|
369
370
|
});
|
|
370
371
|
continue;
|
|
371
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
|
+
}
|
|
372
380
|
if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
|
|
373
381
|
if (typeof message.content === 'string' && message.content.length > 0) {
|
|
374
382
|
input.push({
|
|
@@ -378,9 +386,11 @@ function toResponsesInput(messages) {
|
|
|
378
386
|
});
|
|
379
387
|
}
|
|
380
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`.
|
|
381
392
|
input.push({
|
|
382
393
|
type: 'function_call',
|
|
383
|
-
id: call.id,
|
|
384
394
|
call_id: call.id,
|
|
385
395
|
name: call.name,
|
|
386
396
|
arguments: JSON.stringify(call.arguments)
|
|
@@ -446,18 +456,35 @@ function toResponsesTools(tools) {
|
|
|
446
456
|
}
|
|
447
457
|
function mapResponsesTextResponse(response, req) {
|
|
448
458
|
const toolCalls = extractResponsesToolCalls(response, req, 'text');
|
|
459
|
+
const providerItems = toResponsesProviderItems(response.output, toolCalls);
|
|
449
460
|
return {
|
|
450
461
|
content: extractResponsesText(response),
|
|
451
462
|
...(toolCalls ? { toolCalls } : {}),
|
|
463
|
+
...(providerItems ? { providerItems } : {}),
|
|
452
464
|
usage: toResponsesUsage(response.usage),
|
|
453
465
|
finishReason: toResponsesFinishReason(response),
|
|
454
466
|
raw: response
|
|
455
467
|
};
|
|
456
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
|
+
}
|
|
457
483
|
async function* streamResponsesText(client, req) {
|
|
458
484
|
const stream = await createResponse(client, req, true);
|
|
459
485
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
460
486
|
let finishReason = 'stop';
|
|
487
|
+
let completedOutput;
|
|
461
488
|
const toolState = new Map();
|
|
462
489
|
for await (const event of stream) {
|
|
463
490
|
req.signal.throwIfAborted();
|
|
@@ -476,21 +503,25 @@ async function* streamResponsesText(client, req) {
|
|
|
476
503
|
else if (event.type === 'response.completed') {
|
|
477
504
|
usage = toResponsesUsage(event.response?.usage);
|
|
478
505
|
finishReason = toResponsesFinishReason(event.response);
|
|
506
|
+
completedOutput = event.response?.output;
|
|
479
507
|
}
|
|
480
508
|
else if (event.type === 'response.failed' || event.type === 'response.incomplete') {
|
|
481
509
|
finishReason = 'error';
|
|
482
510
|
}
|
|
483
511
|
}
|
|
484
|
-
|
|
512
|
+
const toolCalls = finalizeResponsesStreamToolCalls(toolState, req, 'textStream');
|
|
513
|
+
for (const call of toolCalls) {
|
|
485
514
|
yield { kind: 'tool_call', call };
|
|
486
515
|
}
|
|
487
|
-
|
|
516
|
+
const providerItems = toResponsesProviderItems(completedOutput, toolCalls);
|
|
517
|
+
yield { kind: 'finish', usage, finishReason, ...(providerItems ? { providerItems } : {}) };
|
|
488
518
|
}
|
|
489
519
|
async function* streamResponsesObject(client, req) {
|
|
490
520
|
const stream = await createResponse(client, req, true);
|
|
491
521
|
let partial = '';
|
|
492
522
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
493
523
|
let finishReason = 'stop';
|
|
524
|
+
let completedOutput;
|
|
494
525
|
const toolState = new Map();
|
|
495
526
|
for await (const event of stream) {
|
|
496
527
|
req.signal.throwIfAborted();
|
|
@@ -510,16 +541,19 @@ async function* streamResponsesObject(client, req) {
|
|
|
510
541
|
else if (event.type === 'response.completed') {
|
|
511
542
|
usage = toResponsesUsage(event.response?.usage);
|
|
512
543
|
finishReason = toResponsesFinishReason(event.response);
|
|
544
|
+
completedOutput = event.response?.output;
|
|
513
545
|
}
|
|
514
546
|
else if (event.type === 'response.failed' || event.type === 'response.incomplete') {
|
|
515
547
|
finishReason = 'error';
|
|
516
548
|
}
|
|
517
549
|
}
|
|
518
|
-
|
|
550
|
+
const toolCalls = finalizeResponsesStreamToolCalls(toolState, req, 'objectStream');
|
|
551
|
+
for (const call of toolCalls) {
|
|
519
552
|
yield { kind: 'tool_call', call };
|
|
520
553
|
}
|
|
521
554
|
const object = parseJson(partial || '{}', req, 'objectStream');
|
|
522
|
-
|
|
555
|
+
const providerItems = toResponsesProviderItems(completedOutput, toolCalls);
|
|
556
|
+
yield { kind: 'finish', object, usage, finishReason, ...(providerItems ? { providerItems } : {}) };
|
|
523
557
|
}
|
|
524
558
|
function extractResponsesText(response) {
|
|
525
559
|
if (typeof response.output_text === 'string')
|
|
@@ -569,8 +603,11 @@ function accumulateResponsesToolCallDelta(state, event) {
|
|
|
569
603
|
function accumulateResponsesToolCallDone(state, event) {
|
|
570
604
|
const index = typeof event.output_index === 'number' ? event.output_index : 0;
|
|
571
605
|
const existing = state.get(index) ?? { args: '' };
|
|
572
|
-
|
|
573
|
-
|
|
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);
|
|
574
611
|
if (typeof event.arguments === 'string')
|
|
575
612
|
existing.args = event.arguments;
|
|
576
613
|
state.set(index, existing);
|
|
@@ -578,12 +615,17 @@ function accumulateResponsesToolCallDone(state, event) {
|
|
|
578
615
|
function finalizeResponsesStreamToolCalls(state, req, method) {
|
|
579
616
|
return [...state.entries()]
|
|
580
617
|
.sort((a, b) => a[0] - b[0])
|
|
581
|
-
.filter(([, call]) => call.
|
|
582
|
-
.map(([, call]) =>
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
+
});
|
|
587
629
|
}
|
|
588
630
|
function accumulateToolCallDeltas(state, deltas) {
|
|
589
631
|
for (const delta of deltas) {
|