websocket.mjs 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import { OpenAI } from "../index.mjs";
  2. import { OpenAIError } from "../error.mjs";
  3. import { OpenAIRealtimeEmitter, buildRealtimeURL, isAzure } from "./internal-base.mjs";
  4. import { isRunningInBrowser } from "../internal/detect-platform.mjs";
  5. export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter {
  6. constructor(props, client) {
  7. super();
  8. const hasProvider = typeof client?._options?.apiKey === 'function';
  9. const dangerouslyAllowBrowser = props.dangerouslyAllowBrowser ??
  10. client?._options?.dangerouslyAllowBrowser ??
  11. (client?.apiKey?.startsWith('ek_') ? true : null);
  12. if (!dangerouslyAllowBrowser && isRunningInBrowser()) {
  13. throw new OpenAIError("It looks like you're running in a browser-like environment.\n\nThis is disabled by default, as it risks exposing your secret API credentials to attackers.\n\nYou can avoid this error by creating an ephemeral session token:\nhttps://platform.openai.com/docs/api-reference/realtime-sessions\n");
  14. }
  15. client ?? (client = new OpenAI({ dangerouslyAllowBrowser }));
  16. if (hasProvider && !props?.__resolvedApiKey) {
  17. throw new Error([
  18. 'Cannot open Realtime WebSocket with a function-based apiKey.',
  19. 'Use the .create() method so that the key is resolved before connecting:',
  20. 'await OpenAIRealtimeWebSocket.create(client, { model })',
  21. ].join('\n'));
  22. }
  23. this.url = buildRealtimeURL(client, props.model);
  24. props.onURL?.(this.url);
  25. // @ts-ignore
  26. this.socket = new WebSocket(this.url.toString(), [
  27. 'realtime',
  28. ...(isAzure(client) ? [] : [`openai-insecure-api-key.${client.apiKey}`]),
  29. ]);
  30. this.socket.addEventListener('message', (websocketEvent) => {
  31. const event = (() => {
  32. try {
  33. return JSON.parse(websocketEvent.data.toString());
  34. }
  35. catch (err) {
  36. this._onError(null, 'could not parse websocket event', err);
  37. return null;
  38. }
  39. })();
  40. if (event) {
  41. this._emit('event', event);
  42. if (event.type === 'error') {
  43. this._onError(event);
  44. }
  45. else {
  46. // @ts-expect-error TS isn't smart enough to get the relationship right here
  47. this._emit(event.type, event);
  48. }
  49. }
  50. });
  51. this.socket.addEventListener('error', (event) => {
  52. this._onError(null, event.message, null);
  53. });
  54. if (isAzure(client)) {
  55. if (this.url.searchParams.get('Authorization') !== null) {
  56. this.url.searchParams.set('Authorization', '<REDACTED>');
  57. }
  58. else {
  59. this.url.searchParams.set('api-key', '<REDACTED>');
  60. }
  61. }
  62. }
  63. static async create(client, props) {
  64. return new OpenAIRealtimeWebSocket({ ...props, __resolvedApiKey: await client._callApiKey() }, client);
  65. }
  66. static async azure(client, options = {}) {
  67. const isApiKeyProvider = await client._callApiKey();
  68. function onURL(url) {
  69. if (isApiKeyProvider) {
  70. url.searchParams.set('Authorization', `Bearer ${client.apiKey}`);
  71. }
  72. else {
  73. url.searchParams.set('api-key', client.apiKey);
  74. }
  75. }
  76. const deploymentName = options.deploymentName ?? client.deploymentName;
  77. if (!deploymentName) {
  78. throw new Error('No deployment name provided');
  79. }
  80. const { dangerouslyAllowBrowser } = options;
  81. return new OpenAIRealtimeWebSocket({
  82. model: deploymentName,
  83. onURL,
  84. ...(dangerouslyAllowBrowser ? { dangerouslyAllowBrowser } : {}),
  85. __resolvedApiKey: isApiKeyProvider,
  86. }, client);
  87. }
  88. send(event) {
  89. try {
  90. this.socket.send(JSON.stringify(event));
  91. }
  92. catch (err) {
  93. this._onError(null, 'could not send data', err);
  94. }
  95. }
  96. close(props) {
  97. try {
  98. this.socket.close(props?.code ?? 1000, props?.reason ?? 'OK');
  99. }
  100. catch (err) {
  101. this._onError(null, 'could not close the connection', err);
  102. }
  103. }
  104. }
  105. //# sourceMappingURL=websocket.mjs.map