@openai/agents-openai 0.4.14 → 0.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/dist/defaults.d.ts +4 -0
- package/dist/defaults.js +15 -1
- package/dist/defaults.js.map +1 -1
- package/dist/defaults.mjs +11 -0
- package/dist/defaults.mjs.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -2
- package/dist/index.mjs.map +1 -1
- package/dist/metadata.js +2 -2
- package/dist/metadata.js.map +1 -1
- package/dist/metadata.mjs +2 -2
- package/dist/metadata.mjs.map +1 -1
- package/dist/openaiProvider.d.ts +7 -0
- package/dist/openaiProvider.js +84 -2
- package/dist/openaiProvider.js.map +1 -1
- package/dist/openaiProvider.mjs +86 -4
- package/dist/openaiProvider.mjs.map +1 -1
- package/dist/openaiResponsesModel.d.ts +28 -1
- package/dist/openaiResponsesModel.js +435 -33
- package/dist/openaiResponsesModel.js.map +1 -1
- package/dist/openaiResponsesModel.mjs +433 -32
- package/dist/openaiResponsesModel.mjs.map +1 -1
- package/dist/responsesTransportUtils.d.ts +29 -0
- package/dist/responsesTransportUtils.js +208 -0
- package/dist/responsesTransportUtils.js.map +1 -0
- package/dist/responsesTransportUtils.mjs +198 -0
- package/dist/responsesTransportUtils.mjs.map +1 -0
- package/dist/responsesWebSocketConnection.d.ts +22 -0
- package/dist/responsesWebSocketConnection.js +273 -0
- package/dist/responsesWebSocketConnection.js.map +1 -0
- package/dist/responsesWebSocketConnection.mjs +259 -0
- package/dist/responsesWebSocketConnection.mjs.map +1 -0
- package/dist/responsesWebSocketSession.d.ts +22 -0
- package/dist/responsesWebSocketSession.js +56 -0
- package/dist/responsesWebSocketSession.js.map +1 -0
- package/dist/responsesWebSocketSession.mjs +53 -0
- package/dist/responsesWebSocketSession.mjs.map +1 -0
- package/package.json +2 -2
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { RequestUsage, Usage, withResponseSpan, createResponseSpan, setCurrentSpan, resetCurrentSpan, UserError, } from '@openai/agents-core';
|
|
2
|
+
import OpenAI from 'openai';
|
|
2
3
|
import logger from "./logger.mjs";
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
import { HEADERS } from "./defaults.mjs";
|
|
6
|
+
import { ResponsesWebSocketConnection, ResponsesWebSocketInternalError, isWebSocketNotOpenError, shouldWrapNoEventWebSocketError, throwIfAborted, webSocketFrameToText, withAbortSignal, withTimeout, } from "./responsesWebSocketConnection.mjs";
|
|
7
|
+
import { applyHeadersToAccumulator, createHeaderAccumulator, ensureResponsesWebSocketPath, headerAccumulatorToRecord, headerAccumulatorToSDKHeaders, mergeQueryParamsIntoURL, splitResponsesTransportOverrides, } from "./responsesTransportUtils.mjs";
|
|
5
8
|
import { CodeInterpreterStatus, FileSearchStatus, ImageGenerationStatus, WebSearchStatus, } from "./tools.mjs";
|
|
6
9
|
import { camelOrSnakeToSnakeCase } from "./utils/providerData.mjs";
|
|
7
10
|
import { encodeUint8ArrayToBase64 } from '@openai/agents-core/utils';
|
|
@@ -1390,23 +1393,51 @@ function convertToOutputItem(items) {
|
|
|
1390
1393
|
});
|
|
1391
1394
|
}
|
|
1392
1395
|
export { getToolChoice, converTool, getInputItems, convertToOutputItem };
|
|
1396
|
+
const TERMINAL_RESPONSES_STREAM_EVENT_TYPES = new Set([
|
|
1397
|
+
'response.completed',
|
|
1398
|
+
'response.failed',
|
|
1399
|
+
'response.incomplete',
|
|
1400
|
+
'response.error',
|
|
1401
|
+
]);
|
|
1402
|
+
function isTerminalResponsesStreamEventType(eventType) {
|
|
1403
|
+
return (typeof eventType === 'string' &&
|
|
1404
|
+
TERMINAL_RESPONSES_STREAM_EVENT_TYPES.has(eventType));
|
|
1405
|
+
}
|
|
1393
1406
|
/**
|
|
1394
1407
|
* Model implementation that uses OpenAI's Responses API to generate responses.
|
|
1395
1408
|
*/
|
|
1396
1409
|
export class OpenAIResponsesModel {
|
|
1397
|
-
|
|
1398
|
-
|
|
1410
|
+
_client;
|
|
1411
|
+
_model;
|
|
1399
1412
|
constructor(client, model) {
|
|
1400
|
-
this
|
|
1401
|
-
this
|
|
1413
|
+
this._client = client;
|
|
1414
|
+
this._model = model;
|
|
1402
1415
|
}
|
|
1403
|
-
async
|
|
1416
|
+
async _fetchResponse(request, stream) {
|
|
1417
|
+
const builtRequest = this._buildResponsesCreateRequest(request, stream);
|
|
1418
|
+
const response = await this._client.responses.create(builtRequest.requestData, {
|
|
1419
|
+
headers: builtRequest.sdkRequestHeaders,
|
|
1420
|
+
signal: builtRequest.signal,
|
|
1421
|
+
...(builtRequest.transportExtraQuery
|
|
1422
|
+
? { query: builtRequest.transportExtraQuery }
|
|
1423
|
+
: {}),
|
|
1424
|
+
});
|
|
1425
|
+
if (logger.dontLogModelData) {
|
|
1426
|
+
logger.debug('Response received');
|
|
1427
|
+
}
|
|
1428
|
+
else {
|
|
1429
|
+
logger.debug(`Response received: ${JSON.stringify(response, null, 2)}`);
|
|
1430
|
+
}
|
|
1431
|
+
return response;
|
|
1432
|
+
}
|
|
1433
|
+
_buildResponsesCreateRequest(request, stream) {
|
|
1404
1434
|
const input = getInputItems(request.input);
|
|
1405
1435
|
const { tools, include } = getTools(request.tools, request.handoffs);
|
|
1406
1436
|
const toolChoice = getToolChoice(request.modelSettings.toolChoice);
|
|
1407
|
-
const {
|
|
1437
|
+
const { providerData: providerDataWithoutTransport, overrides: transportOverrides, } = splitResponsesTransportOverrides(request.modelSettings.providerData);
|
|
1438
|
+
const { text, ...restOfProviderData } = providerDataWithoutTransport;
|
|
1408
1439
|
if (request.modelSettings.reasoning) {
|
|
1409
|
-
// Merge top-level reasoning settings with provider data
|
|
1440
|
+
// Merge top-level reasoning settings with provider data.
|
|
1410
1441
|
restOfProviderData.reasoning = {
|
|
1411
1442
|
...request.modelSettings.reasoning,
|
|
1412
1443
|
...restOfProviderData.reasoning,
|
|
@@ -1414,7 +1445,7 @@ export class OpenAIResponsesModel {
|
|
|
1414
1445
|
}
|
|
1415
1446
|
let mergedText = text;
|
|
1416
1447
|
if (request.modelSettings.text) {
|
|
1417
|
-
// Merge top-level text settings with provider data
|
|
1448
|
+
// Merge top-level text settings with provider data.
|
|
1418
1449
|
mergedText = { ...request.modelSettings.text, ...text };
|
|
1419
1450
|
}
|
|
1420
1451
|
const responseFormat = getResponseFormat(request.outputType, mergedText);
|
|
@@ -1435,8 +1466,8 @@ export class OpenAIResponsesModel {
|
|
|
1435
1466
|
const shouldOmitToolChoice = Boolean(request.prompt) &&
|
|
1436
1467
|
!shouldSendTools &&
|
|
1437
1468
|
typeof toolChoice === 'object';
|
|
1438
|
-
|
|
1439
|
-
...(shouldSendModel ? { model: this
|
|
1469
|
+
let requestData = {
|
|
1470
|
+
...(shouldSendModel ? { model: this._model } : {}),
|
|
1440
1471
|
instructions: normalizeInstructions(request.systemInstructions),
|
|
1441
1472
|
input,
|
|
1442
1473
|
include,
|
|
@@ -1462,23 +1493,34 @@ export class OpenAIResponsesModel {
|
|
|
1462
1493
|
prompt_cache_retention: request.modelSettings.promptCacheRetention,
|
|
1463
1494
|
...restOfProviderData,
|
|
1464
1495
|
};
|
|
1465
|
-
if (
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1496
|
+
if (transportOverrides.extraBody) {
|
|
1497
|
+
requestData = {
|
|
1498
|
+
...requestData,
|
|
1499
|
+
...transportOverrides.extraBody,
|
|
1500
|
+
};
|
|
1470
1501
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1502
|
+
// Keep the transport mode aligned with the calling path even if extra_body includes stream.
|
|
1503
|
+
requestData.stream = stream;
|
|
1504
|
+
const requestHeaderAccumulator = createHeaderAccumulator();
|
|
1505
|
+
applyHeadersToAccumulator(requestHeaderAccumulator, HEADERS);
|
|
1506
|
+
applyHeadersToAccumulator(requestHeaderAccumulator, transportOverrides.extraHeaders, {
|
|
1507
|
+
allowBlockedOverride: true,
|
|
1474
1508
|
});
|
|
1509
|
+
const sdkRequestHeaders = headerAccumulatorToSDKHeaders(requestHeaderAccumulator);
|
|
1510
|
+
const builtRequest = {
|
|
1511
|
+
requestData,
|
|
1512
|
+
sdkRequestHeaders,
|
|
1513
|
+
signal: request.signal,
|
|
1514
|
+
transportExtraHeaders: transportOverrides.extraHeaders,
|
|
1515
|
+
transportExtraQuery: transportOverrides.extraQuery,
|
|
1516
|
+
};
|
|
1475
1517
|
if (logger.dontLogModelData) {
|
|
1476
|
-
logger.debug('
|
|
1518
|
+
logger.debug('Calling LLM');
|
|
1477
1519
|
}
|
|
1478
1520
|
else {
|
|
1479
|
-
logger.debug(`
|
|
1521
|
+
logger.debug(`Calling LLM. Request data: ${JSON.stringify(builtRequest.requestData, null, 2)}`);
|
|
1480
1522
|
}
|
|
1481
|
-
return
|
|
1523
|
+
return builtRequest;
|
|
1482
1524
|
}
|
|
1483
1525
|
/**
|
|
1484
1526
|
* Get a response from the OpenAI model using the Responses API.
|
|
@@ -1487,7 +1529,7 @@ export class OpenAIResponsesModel {
|
|
|
1487
1529
|
*/
|
|
1488
1530
|
async getResponse(request) {
|
|
1489
1531
|
const response = await withResponseSpan(async (span) => {
|
|
1490
|
-
const response = await this
|
|
1532
|
+
const response = await this._fetchResponse(request, false);
|
|
1491
1533
|
if (request.tracing) {
|
|
1492
1534
|
span.spanData.response_id = response.id;
|
|
1493
1535
|
span.spanData._input = request.input;
|
|
@@ -1527,10 +1569,11 @@ export class OpenAIResponsesModel {
|
|
|
1527
1569
|
span.spanData._input = request.input;
|
|
1528
1570
|
}
|
|
1529
1571
|
}
|
|
1530
|
-
const response = await this
|
|
1572
|
+
const response = await this._fetchResponse(request, true);
|
|
1531
1573
|
let finalResponse;
|
|
1532
1574
|
for await (const event of response) {
|
|
1533
|
-
|
|
1575
|
+
const eventType = event.type;
|
|
1576
|
+
if (eventType === 'response.created') {
|
|
1534
1577
|
yield {
|
|
1535
1578
|
type: 'response_started',
|
|
1536
1579
|
providerData: {
|
|
@@ -1538,9 +1581,10 @@ export class OpenAIResponsesModel {
|
|
|
1538
1581
|
},
|
|
1539
1582
|
};
|
|
1540
1583
|
}
|
|
1541
|
-
else if (
|
|
1542
|
-
|
|
1543
|
-
|
|
1584
|
+
else if (isTerminalResponsesStreamEventType(eventType)) {
|
|
1585
|
+
const terminalEvent = event;
|
|
1586
|
+
finalResponse = terminalEvent.response;
|
|
1587
|
+
const { response, ...remainingEvent } = terminalEvent;
|
|
1544
1588
|
const { output, usage, id, ...remainingResponse } = response;
|
|
1545
1589
|
yield {
|
|
1546
1590
|
type: 'response_done',
|
|
@@ -1565,12 +1609,14 @@ export class OpenAIResponsesModel {
|
|
|
1565
1609
|
},
|
|
1566
1610
|
providerData: remainingEvent,
|
|
1567
1611
|
};
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1612
|
+
if (eventType === 'response.completed') {
|
|
1613
|
+
yield {
|
|
1614
|
+
type: 'model',
|
|
1615
|
+
event: event,
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1572
1618
|
}
|
|
1573
|
-
else if (
|
|
1619
|
+
else if (eventType === 'response.output_text.delta') {
|
|
1574
1620
|
const { delta, ...remainingEvent } = event;
|
|
1575
1621
|
yield {
|
|
1576
1622
|
type: 'output_text_delta',
|
|
@@ -1611,6 +1657,343 @@ export class OpenAIResponsesModel {
|
|
|
1611
1657
|
}
|
|
1612
1658
|
}
|
|
1613
1659
|
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Model implementation that uses the OpenAI Responses API over a websocket transport.
|
|
1662
|
+
*
|
|
1663
|
+
* @see {@link https://developers.openai.com/api/docs/guides/websocket-mode}
|
|
1664
|
+
*/
|
|
1665
|
+
export class OpenAIResponsesWSModel extends OpenAIResponsesModel {
|
|
1666
|
+
#websocketBaseURL;
|
|
1667
|
+
#reuseConnection;
|
|
1668
|
+
#wsConnection;
|
|
1669
|
+
#wsConnectionIdentity;
|
|
1670
|
+
#wsRequestLock = Promise.resolve();
|
|
1671
|
+
constructor(client, model, options = {}) {
|
|
1672
|
+
super(client, model);
|
|
1673
|
+
this.#websocketBaseURL = options.websocketBaseURL;
|
|
1674
|
+
this.#reuseConnection = options.reuseConnection ?? true;
|
|
1675
|
+
}
|
|
1676
|
+
async _fetchResponse(request, stream) {
|
|
1677
|
+
// The websocket transport always uses streamed Responses events, then callers either
|
|
1678
|
+
// consume the stream directly or collapse it into the final terminal response.
|
|
1679
|
+
const builtRequest = this._buildResponsesCreateRequest(request, true);
|
|
1680
|
+
if (stream) {
|
|
1681
|
+
return this.#iterWebSocketResponseEvents(builtRequest);
|
|
1682
|
+
}
|
|
1683
|
+
let finalResponse;
|
|
1684
|
+
for await (const event of this.#iterWebSocketResponseEvents(builtRequest)) {
|
|
1685
|
+
const eventType = event.type;
|
|
1686
|
+
if (isTerminalResponsesStreamEventType(eventType)) {
|
|
1687
|
+
finalResponse = event
|
|
1688
|
+
.response;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
if (!finalResponse) {
|
|
1692
|
+
throw new Error('Responses websocket stream ended without a terminal response event.');
|
|
1693
|
+
}
|
|
1694
|
+
return finalResponse;
|
|
1695
|
+
}
|
|
1696
|
+
async close() {
|
|
1697
|
+
await this.#dropWebSocketConnection();
|
|
1698
|
+
}
|
|
1699
|
+
async *#iterWebSocketResponseEvents(builtRequest) {
|
|
1700
|
+
const requestTimeoutDeadline = this.#createWebSocketRequestTimeoutDeadline();
|
|
1701
|
+
const releaseLock = await this.#acquireWebSocketRequestLock(builtRequest.signal, requestTimeoutDeadline);
|
|
1702
|
+
let receivedAnyEvent = false;
|
|
1703
|
+
let sawTerminalResponseEvent = false;
|
|
1704
|
+
try {
|
|
1705
|
+
throwIfAborted(builtRequest.signal);
|
|
1706
|
+
const { frame, wsURL, headers } = await this.#prepareWebSocketRequest(builtRequest, requestTimeoutDeadline);
|
|
1707
|
+
throwIfAborted(builtRequest.signal);
|
|
1708
|
+
let connection = await this.#ensureWebSocketConnection(wsURL, headers, builtRequest.signal, requestTimeoutDeadline);
|
|
1709
|
+
let reusedConnectionForCurrentAttempt = connection.reused;
|
|
1710
|
+
let activeConnection = connection.connection;
|
|
1711
|
+
const setActiveConnection = (nextConnection) => {
|
|
1712
|
+
connection = nextConnection;
|
|
1713
|
+
activeConnection = nextConnection.connection;
|
|
1714
|
+
reusedConnectionForCurrentAttempt = nextConnection.reused;
|
|
1715
|
+
};
|
|
1716
|
+
throwIfAborted(builtRequest.signal);
|
|
1717
|
+
const serializedFrame = JSON.stringify(frame);
|
|
1718
|
+
const sendSerializedFrame = async () => {
|
|
1719
|
+
try {
|
|
1720
|
+
await activeConnection.send(serializedFrame);
|
|
1721
|
+
}
|
|
1722
|
+
catch (error) {
|
|
1723
|
+
if (!isWebSocketNotOpenError(error)) {
|
|
1724
|
+
throw error;
|
|
1725
|
+
}
|
|
1726
|
+
setActiveConnection(await this.#reconnectWebSocketConnection(wsURL, headers, builtRequest.signal, requestTimeoutDeadline));
|
|
1727
|
+
await activeConnection.send(serializedFrame);
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
await sendSerializedFrame();
|
|
1731
|
+
while (true) {
|
|
1732
|
+
const rawFrame = await this.#nextWebSocketFrame(activeConnection, builtRequest.signal, requestTimeoutDeadline);
|
|
1733
|
+
if (rawFrame === null) {
|
|
1734
|
+
if (!receivedAnyEvent && reusedConnectionForCurrentAttempt) {
|
|
1735
|
+
// The request frame was already sent on a reused socket. If the
|
|
1736
|
+
// socket closes before the first response event arrives, the server
|
|
1737
|
+
// may still be processing the request, so replaying `response.create`
|
|
1738
|
+
// can duplicate model work and tool side effects.
|
|
1739
|
+
receivedAnyEvent = true;
|
|
1740
|
+
throw new Error('Responses websocket connection closed after sending a request on a reused connection before any response events were received. The request may have been accepted, so the SDK will not automatically retry this websocket request.');
|
|
1741
|
+
}
|
|
1742
|
+
throw new ResponsesWebSocketInternalError('connection_closed_before_terminal_response_event', 'Responses websocket connection closed before a terminal response event.');
|
|
1743
|
+
}
|
|
1744
|
+
const payloadText = await webSocketFrameToText(rawFrame);
|
|
1745
|
+
const payload = JSON.parse(payloadText);
|
|
1746
|
+
const eventType = isRecord(payload) && typeof payload.type === 'string'
|
|
1747
|
+
? payload.type
|
|
1748
|
+
: undefined;
|
|
1749
|
+
if (eventType === 'error') {
|
|
1750
|
+
receivedAnyEvent = true;
|
|
1751
|
+
throw new Error(`Responses websocket error: ${JSON.stringify(payload)}`);
|
|
1752
|
+
}
|
|
1753
|
+
const event = payload;
|
|
1754
|
+
const isTerminalResponseEvent = isTerminalResponsesStreamEventType(eventType);
|
|
1755
|
+
receivedAnyEvent = true;
|
|
1756
|
+
if (isTerminalResponseEvent) {
|
|
1757
|
+
sawTerminalResponseEvent = true;
|
|
1758
|
+
}
|
|
1759
|
+
yield event;
|
|
1760
|
+
if (isTerminalResponseEvent) {
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
catch (error) {
|
|
1766
|
+
if (!receivedAnyEvent &&
|
|
1767
|
+
!(error instanceof OpenAI.APIUserAbortError) &&
|
|
1768
|
+
shouldWrapNoEventWebSocketError(error)) {
|
|
1769
|
+
const wrappedError = new Error('Responses websocket connection closed before any response events were received. The feature may not be enabled for this account or model yet.');
|
|
1770
|
+
if (error instanceof Error) {
|
|
1771
|
+
wrappedError.cause = error;
|
|
1772
|
+
}
|
|
1773
|
+
throw wrappedError;
|
|
1774
|
+
}
|
|
1775
|
+
throw error;
|
|
1776
|
+
}
|
|
1777
|
+
finally {
|
|
1778
|
+
const shouldDropConnection = !sawTerminalResponseEvent || !this.#reuseConnection;
|
|
1779
|
+
const dropConnectionPromise = shouldDropConnection
|
|
1780
|
+
? this.#dropWebSocketConnection()
|
|
1781
|
+
: undefined;
|
|
1782
|
+
releaseLock();
|
|
1783
|
+
await dropConnectionPromise;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
async #prepareWebSocketRequest(builtRequest, requestTimeoutDeadline) {
|
|
1787
|
+
const wsURL = this.#prepareWebSocketURL(builtRequest.transportExtraQuery);
|
|
1788
|
+
const headers = await this.#mergeWebSocketHeaders(wsURL, builtRequest.transportExtraHeaders, builtRequest.signal, requestTimeoutDeadline);
|
|
1789
|
+
const frame = {
|
|
1790
|
+
...builtRequest.requestData,
|
|
1791
|
+
type: 'response.create',
|
|
1792
|
+
stream: true,
|
|
1793
|
+
};
|
|
1794
|
+
return { frame, wsURL, headers };
|
|
1795
|
+
}
|
|
1796
|
+
async #mergeWebSocketHeaders(wsURL, extraHeaders, signal, requestTimeoutDeadline) {
|
|
1797
|
+
await this.#awaitWebSocketRequestTimedOperation(this.#refreshClientApiKey(), signal, requestTimeoutDeadline, (configuredTimeoutMs) => `Responses websocket auth header preparation timed out after ${configuredTimeoutMs}ms.`);
|
|
1798
|
+
const headerAccumulator = createHeaderAccumulator();
|
|
1799
|
+
const clientWithInternals = this._client;
|
|
1800
|
+
const handshakeURL = new URL(wsURL);
|
|
1801
|
+
const handshakeQuery = searchParamsToAuthHeaderQuery(handshakeURL.searchParams);
|
|
1802
|
+
const authHeaders = typeof clientWithInternals.authHeaders === 'function'
|
|
1803
|
+
? await this.#awaitWebSocketRequestTimedOperation(clientWithInternals.authHeaders({
|
|
1804
|
+
method: 'get',
|
|
1805
|
+
path: handshakeURL.pathname,
|
|
1806
|
+
...(handshakeQuery ? { query: handshakeQuery } : {}),
|
|
1807
|
+
}), signal, requestTimeoutDeadline, (configuredTimeoutMs) => `Responses websocket auth header preparation timed out after ${configuredTimeoutMs}ms.`)
|
|
1808
|
+
: undefined;
|
|
1809
|
+
applyHeadersToAccumulator(headerAccumulator, authHeaders);
|
|
1810
|
+
if (typeof clientWithInternals.authHeaders !== 'function' &&
|
|
1811
|
+
typeof this._client.apiKey === 'string' &&
|
|
1812
|
+
this._client.apiKey.length > 0 &&
|
|
1813
|
+
this._client.apiKey !== 'Missing Key') {
|
|
1814
|
+
applyHeadersToAccumulator(headerAccumulator, {
|
|
1815
|
+
Authorization: `Bearer ${this._client.apiKey}`,
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
if (this._client.organization) {
|
|
1819
|
+
applyHeadersToAccumulator(headerAccumulator, {
|
|
1820
|
+
'OpenAI-Organization': this._client.organization,
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
if (this._client.project) {
|
|
1824
|
+
applyHeadersToAccumulator(headerAccumulator, {
|
|
1825
|
+
'OpenAI-Project': this._client.project,
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
applyHeadersToAccumulator(headerAccumulator, clientWithInternals._options?.defaultHeaders);
|
|
1829
|
+
applyHeadersToAccumulator(headerAccumulator, HEADERS);
|
|
1830
|
+
applyHeadersToAccumulator(headerAccumulator, extraHeaders, {
|
|
1831
|
+
allowBlockedOverride: true,
|
|
1832
|
+
});
|
|
1833
|
+
return headerAccumulatorToRecord(headerAccumulator);
|
|
1834
|
+
}
|
|
1835
|
+
#prepareWebSocketURL(extraQuery) {
|
|
1836
|
+
const baseURL = new URL(this.#websocketBaseURL ?? this._client.baseURL);
|
|
1837
|
+
const explicitBaseQuery = typeof this.#websocketBaseURL === 'string'
|
|
1838
|
+
? new URLSearchParams(baseURL.search)
|
|
1839
|
+
: undefined;
|
|
1840
|
+
const clientWithInternals = this._client;
|
|
1841
|
+
if (baseURL.protocol === 'https:') {
|
|
1842
|
+
baseURL.protocol = 'wss:';
|
|
1843
|
+
}
|
|
1844
|
+
else if (baseURL.protocol === 'http:') {
|
|
1845
|
+
baseURL.protocol = 'ws:';
|
|
1846
|
+
}
|
|
1847
|
+
else if (baseURL.protocol !== 'ws:' && baseURL.protocol !== 'wss:') {
|
|
1848
|
+
throw new UserError(`Unsupported websocket base URL protocol: ${baseURL.protocol}`);
|
|
1849
|
+
}
|
|
1850
|
+
baseURL.pathname = ensureResponsesWebSocketPath(baseURL.pathname);
|
|
1851
|
+
mergeQueryParamsIntoURL(baseURL, clientWithInternals._options?.defaultQuery);
|
|
1852
|
+
if (explicitBaseQuery && Array.from(explicitBaseQuery.keys()).length > 0) {
|
|
1853
|
+
const explicitTopLevelKeys = new Set();
|
|
1854
|
+
for (const key of explicitBaseQuery.keys()) {
|
|
1855
|
+
const bracketIndex = key.indexOf('[');
|
|
1856
|
+
explicitTopLevelKeys.add(bracketIndex >= 0 ? key.slice(0, bracketIndex) : key);
|
|
1857
|
+
}
|
|
1858
|
+
for (const topLevelKey of explicitTopLevelKeys) {
|
|
1859
|
+
for (const existingKey of Array.from(baseURL.searchParams.keys())) {
|
|
1860
|
+
if (existingKey === topLevelKey ||
|
|
1861
|
+
existingKey.startsWith(`${topLevelKey}[`)) {
|
|
1862
|
+
baseURL.searchParams.delete(existingKey);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
for (const [key, value] of explicitBaseQuery.entries()) {
|
|
1867
|
+
baseURL.searchParams.append(key, value);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
mergeQueryParamsIntoURL(baseURL, extraQuery);
|
|
1871
|
+
return baseURL.toString();
|
|
1872
|
+
}
|
|
1873
|
+
async #ensureWebSocketConnection(wsURL, headers, signal, requestTimeoutDeadline) {
|
|
1874
|
+
const identity = this.#getConnectionIdentity(wsURL, headers);
|
|
1875
|
+
if (this.#wsConnection &&
|
|
1876
|
+
this.#wsConnectionIdentity &&
|
|
1877
|
+
this.#wsConnectionIdentity === identity &&
|
|
1878
|
+
this.#wsConnection.isReusable()) {
|
|
1879
|
+
return { connection: this.#wsConnection, reused: true };
|
|
1880
|
+
}
|
|
1881
|
+
await this.#dropWebSocketConnection();
|
|
1882
|
+
const connectTimeout = this.#resolveWebSocketRequestTimeout(requestTimeoutDeadline, (configuredTimeoutMs) => `Responses websocket connection timed out before opening after ${configuredTimeoutMs}ms.`);
|
|
1883
|
+
this.#wsConnection = await ResponsesWebSocketConnection.connect(wsURL, headers, signal, connectTimeout.timeoutMs, connectTimeout.errorMessage);
|
|
1884
|
+
this.#wsConnectionIdentity = identity;
|
|
1885
|
+
return { connection: this.#wsConnection, reused: false };
|
|
1886
|
+
}
|
|
1887
|
+
async #reconnectWebSocketConnection(wsURL, headers, signal, requestTimeoutDeadline) {
|
|
1888
|
+
await this.#dropWebSocketConnection();
|
|
1889
|
+
throwIfAborted(signal);
|
|
1890
|
+
const connection = await this.#ensureWebSocketConnection(wsURL, headers, signal, requestTimeoutDeadline);
|
|
1891
|
+
throwIfAborted(signal);
|
|
1892
|
+
return connection;
|
|
1893
|
+
}
|
|
1894
|
+
#getConnectionIdentity(wsURL, headers) {
|
|
1895
|
+
const normalizedHeaders = Object.entries(headers)
|
|
1896
|
+
.map(([key, value]) => [key.toLowerCase(), value])
|
|
1897
|
+
.sort(([leftKey, leftValue], [rightKey, rightValue]) => `${leftKey}:${leftValue}`.localeCompare(`${rightKey}:${rightValue}`));
|
|
1898
|
+
return JSON.stringify([wsURL, normalizedHeaders]);
|
|
1899
|
+
}
|
|
1900
|
+
async #dropWebSocketConnection() {
|
|
1901
|
+
const connectionToClose = this.#wsConnection;
|
|
1902
|
+
if (!connectionToClose) {
|
|
1903
|
+
this.#wsConnectionIdentity = undefined;
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
// Detach cached state before awaiting close so queued requests can proceed
|
|
1907
|
+
// without racing against this teardown path.
|
|
1908
|
+
this.#wsConnection = undefined;
|
|
1909
|
+
this.#wsConnectionIdentity = undefined;
|
|
1910
|
+
try {
|
|
1911
|
+
await connectionToClose.close();
|
|
1912
|
+
}
|
|
1913
|
+
catch {
|
|
1914
|
+
// Ignore close errors and reset the cached connection.
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
async #acquireWebSocketRequestLock(signal, requestTimeoutDeadline) {
|
|
1918
|
+
throwIfAborted(signal);
|
|
1919
|
+
const queueWaitTimeout = this.#resolveWebSocketRequestTimeout(requestTimeoutDeadline, (configuredTimeoutMs) => `Responses websocket request queue wait timed out after ${configuredTimeoutMs}ms.`);
|
|
1920
|
+
const previousLock = this.#wsRequestLock;
|
|
1921
|
+
let released = false;
|
|
1922
|
+
let resolveOwnLock;
|
|
1923
|
+
const ownLock = new Promise((resolve) => {
|
|
1924
|
+
resolveOwnLock = resolve;
|
|
1925
|
+
});
|
|
1926
|
+
const releaseLock = () => {
|
|
1927
|
+
if (released) {
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
released = true;
|
|
1931
|
+
resolveOwnLock();
|
|
1932
|
+
};
|
|
1933
|
+
this.#wsRequestLock = previousLock.then(() => ownLock);
|
|
1934
|
+
try {
|
|
1935
|
+
await withAbortSignal(withTimeout(previousLock, queueWaitTimeout.timeoutMs, queueWaitTimeout.errorMessage), signal);
|
|
1936
|
+
throwIfAborted(signal);
|
|
1937
|
+
return releaseLock;
|
|
1938
|
+
}
|
|
1939
|
+
catch (error) {
|
|
1940
|
+
releaseLock();
|
|
1941
|
+
throw error;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
async #refreshClientApiKey() {
|
|
1945
|
+
const clientWithInternals = this._client;
|
|
1946
|
+
if (typeof clientWithInternals._callApiKey === 'function') {
|
|
1947
|
+
await clientWithInternals._callApiKey();
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
#getWebSocketFrameReadTimeoutMs() {
|
|
1951
|
+
const clientWithTimeout = this._client;
|
|
1952
|
+
const timeoutCandidate = typeof clientWithTimeout.timeout === 'number'
|
|
1953
|
+
? clientWithTimeout.timeout
|
|
1954
|
+
: clientWithTimeout._options?.timeout;
|
|
1955
|
+
if (typeof timeoutCandidate === 'number') {
|
|
1956
|
+
return timeoutCandidate;
|
|
1957
|
+
}
|
|
1958
|
+
return OpenAI.DEFAULT_TIMEOUT;
|
|
1959
|
+
}
|
|
1960
|
+
#createWebSocketRequestTimeoutDeadline() {
|
|
1961
|
+
const timeoutMs = this.#getWebSocketFrameReadTimeoutMs();
|
|
1962
|
+
if (typeof timeoutMs !== 'number' ||
|
|
1963
|
+
!Number.isFinite(timeoutMs) ||
|
|
1964
|
+
timeoutMs <= 0) {
|
|
1965
|
+
return undefined;
|
|
1966
|
+
}
|
|
1967
|
+
return {
|
|
1968
|
+
configuredTimeoutMs: timeoutMs,
|
|
1969
|
+
deadlineAtMs: Date.now() + timeoutMs,
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
#resolveWebSocketRequestTimeout(requestTimeoutDeadline, errorMessageForConfiguredTimeout) {
|
|
1973
|
+
const configuredTimeoutMs = requestTimeoutDeadline?.configuredTimeoutMs ??
|
|
1974
|
+
this.#getWebSocketFrameReadTimeoutMs();
|
|
1975
|
+
const safeConfiguredTimeoutMs = typeof configuredTimeoutMs === 'number'
|
|
1976
|
+
? configuredTimeoutMs
|
|
1977
|
+
: OpenAI.DEFAULT_TIMEOUT;
|
|
1978
|
+
const errorMessage = errorMessageForConfiguredTimeout(safeConfiguredTimeoutMs);
|
|
1979
|
+
if (!requestTimeoutDeadline) {
|
|
1980
|
+
return { timeoutMs: configuredTimeoutMs, errorMessage };
|
|
1981
|
+
}
|
|
1982
|
+
const remainingTimeoutMs = Math.ceil(requestTimeoutDeadline.deadlineAtMs - Date.now());
|
|
1983
|
+
if (remainingTimeoutMs <= 0) {
|
|
1984
|
+
throw new Error(errorMessage);
|
|
1985
|
+
}
|
|
1986
|
+
return { timeoutMs: remainingTimeoutMs, errorMessage };
|
|
1987
|
+
}
|
|
1988
|
+
async #awaitWebSocketRequestTimedOperation(promise, signal, requestTimeoutDeadline, errorMessageForConfiguredTimeout) {
|
|
1989
|
+
const timeout = this.#resolveWebSocketRequestTimeout(requestTimeoutDeadline, errorMessageForConfiguredTimeout);
|
|
1990
|
+
return await withAbortSignal(withTimeout(promise, timeout.timeoutMs, timeout.errorMessage), signal);
|
|
1991
|
+
}
|
|
1992
|
+
async #nextWebSocketFrame(connection, signal, requestTimeoutDeadline) {
|
|
1993
|
+
const frameReadTimeout = this.#resolveWebSocketRequestTimeout(requestTimeoutDeadline, (configuredTimeoutMs) => `Responses websocket frame read timed out after ${configuredTimeoutMs}ms.`);
|
|
1994
|
+
return await withTimeout(connection.nextFrame(signal), frameReadTimeout.timeoutMs, frameReadTimeout.errorMessage);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1614
1997
|
/**
|
|
1615
1998
|
* Sending an empty string for instructions can override the prompt parameter.
|
|
1616
1999
|
* Thus, this method checks if the instructions is an empty string and returns undefined if it is.
|
|
@@ -1626,6 +2009,24 @@ function normalizeInstructions(instructions) {
|
|
|
1626
2009
|
}
|
|
1627
2010
|
return undefined;
|
|
1628
2011
|
}
|
|
2012
|
+
function searchParamsToAuthHeaderQuery(searchParams) {
|
|
2013
|
+
const query = {};
|
|
2014
|
+
let hasEntries = false;
|
|
2015
|
+
for (const [key, value] of searchParams.entries()) {
|
|
2016
|
+
hasEntries = true;
|
|
2017
|
+
const existingValue = query[key];
|
|
2018
|
+
if (typeof existingValue === 'undefined') {
|
|
2019
|
+
query[key] = value;
|
|
2020
|
+
continue;
|
|
2021
|
+
}
|
|
2022
|
+
if (Array.isArray(existingValue)) {
|
|
2023
|
+
existingValue.push(value);
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
query[key] = [existingValue, value];
|
|
2027
|
+
}
|
|
2028
|
+
return hasEntries ? query : undefined;
|
|
2029
|
+
}
|
|
1629
2030
|
function toRequestUsageEntry(usage, endpoint) {
|
|
1630
2031
|
return new RequestUsage({
|
|
1631
2032
|
inputTokens: usage?.input_tokens ?? 0,
|