Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7160ebe

Browse files
authoredJan 16, 2025··
feat(client): add Realtime API support (#1266)
1 parent 5e56979 commit 7160ebe

File tree

11 files changed

+560
-3
lines changed

11 files changed

+560
-3
lines changed
 

‎README.md

+87
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,93 @@ main();
8383
If you need to cancel a stream, you can `break` from the loop
8484
or call `stream.controller.abort()`.
8585

86+
## Realtime API beta
87+
88+
The Realtime API enables you to build low-latency, multi-modal conversational experiences. It currently supports text and audio as both input and output, as well as [function calling](https://platform.openai.com/docs/guides/function-calling) through a `WebSocket` connection.
89+
90+
The Realtime API works through a combination of client-sent events and server-sent events. Clients can send events to do things like update session configuration or send text and audio inputs. Server events confirm when audio responses have completed, or when a text response from the model has been received. A full event reference can be found [here](https://platform.openai.com/docs/api-reference/realtime-client-events) and a guide can be found [here](https://platform.openai.com/docs/guides/realtime).
91+
92+
This SDK supports accessing the Realtime API through the [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) or with [ws](https://github.com/websockets/ws).
93+
94+
Basic text based example with `ws`:
95+
96+
```ts
97+
// requires `yarn add ws @types/ws`
98+
import { OpenAIRealtimeWS } from 'openai/beta/realtime/ws';
99+
100+
const rt = new OpenAIRealtimeWS({ model: 'gpt-4o-realtime-preview-2024-12-17' });
101+
102+
// access the underlying `ws.WebSocket` instance
103+
rt.socket.on('open', () => {
104+
console.log('Connection opened!');
105+
rt.send({
106+
type: 'session.update',
107+
session: {
108+
modalities: ['text'],
109+
model: 'gpt-4o-realtime-preview',
110+
},
111+
});
112+
113+
rt.send({
114+
type: 'conversation.item.create',
115+
item: {
116+
type: 'message',
117+
role: 'user',
118+
content: [{ type: 'input_text', text: 'Say a couple paragraphs!' }],
119+
},
120+
});
121+
122+
rt.send({ type: 'response.create' });
123+
});
124+
125+
rt.on('error', (err) => {
126+
// in a real world scenario this should be logged somewhere as you
127+
// likely want to continue procesing events regardless of any errors
128+
throw err;
129+
});
130+
131+
rt.on('session.created', (event) => {
132+
console.log('session created!', event.session);
133+
console.log();
134+
});
135+
136+
rt.on('response.text.delta', (event) => process.stdout.write(event.delta));
137+
rt.on('response.text.done', () => console.log());
138+
139+
rt.on('response.done', () => rt.close());
140+
141+
rt.socket.on('close', () => console.log('\nConnection closed!'));
142+
```
143+
144+
To use the web API `WebSocket` implementation, replace `OpenAIRealtimeWS` with `OpenAIRealtimeWebSocket` and adjust any `rt.socket` access:
145+
146+
```ts
147+
import { OpenAIRealtimeWebSocket } from 'openai/beta/realtime/websocket';
148+
149+
const rt = new OpenAIRealtimeWebSocket({ model: 'gpt-4o-realtime-preview-2024-12-17' });
150+
// ...
151+
rt.socket.addEventListener('open', () => {
152+
// ...
153+
});
154+
```
155+
156+
A full example can be found [here](https://github.com/openai/openai-node/blob/master/examples/realtime/web.ts).
157+
158+
### Realtime error handling
159+
160+
When an error is encountered, either on the client side or returned from the server through the [`error` event](https://platform.openai.com/docs/guides/realtime/realtime-api-beta#handling-errors), the `error` event listener will be fired. However, if you haven't registered an `error` event listener then an `unhandled Promise rejection` error will be thrown.
161+
162+
It is **highly recommended** that you register an `error` event listener and handle errors approriately as typically the underlying connection is still usable.
163+
164+
```ts
165+
const rt = new OpenAIRealtimeWS({ model: 'gpt-4o-realtime-preview-2024-12-17' });
166+
rt.on('error', (err) => {
167+
// in a real world scenario this should be logged somewhere as you
168+
// likely want to continue procesing events regardless of any errors
169+
throw err;
170+
});
171+
```
172+
86173
### Request & Response types
87174

88175
This library includes TypeScript definitions for all request params and response fields. You may import and use them like so:

‎examples/package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
"license": "MIT",
77
"private": true,
88
"dependencies": {
9+
"@azure/identity": "^4.2.0",
910
"express": "^4.18.2",
1011
"next": "^14.1.1",
1112
"openai": "file:..",
12-
"zod-to-json-schema": "^3.21.4",
13-
"@azure/identity": "^4.2.0"
13+
"zod-to-json-schema": "^3.21.4"
1414
},
1515
"devDependencies": {
1616
"@types/body-parser": "^1.19.3",
17-
"@types/express": "^4.17.19"
17+
"@types/express": "^4.17.19",
18+
"@types/web": "^0.0.194"
1819
}
1920
}

‎examples/realtime/websocket.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { OpenAIRealtimeWebSocket } from 'openai/beta/realtime/websocket';
2+
3+
async function main() {
4+
const rt = new OpenAIRealtimeWebSocket({ model: 'gpt-4o-realtime-preview-2024-12-17' });
5+
6+
// access the underlying `ws.WebSocket` instance
7+
rt.socket.addEventListener('open', () => {
8+
console.log('Connection opened!');
9+
rt.send({
10+
type: 'session.update',
11+
session: {
12+
modalities: ['text'],
13+
model: 'gpt-4o-realtime-preview',
14+
},
15+
});
16+
17+
rt.send({
18+
type: 'conversation.item.create',
19+
item: {
20+
type: 'message',
21+
role: 'user',
22+
content: [{ type: 'input_text', text: 'Say a couple paragraphs!' }],
23+
},
24+
});
25+
26+
rt.send({ type: 'response.create' });
27+
});
28+
29+
rt.on('error', (err) => {
30+
// in a real world scenario this should be logged somewhere as you
31+
// likely want to continue procesing events regardless of any errors
32+
throw err;
33+
});
34+
35+
rt.on('session.created', (event) => {
36+
console.log('session created!', event.session);
37+
console.log();
38+
});
39+
40+
rt.on('response.text.delta', (event) => process.stdout.write(event.delta));
41+
rt.on('response.text.done', () => console.log());
42+
43+
rt.on('response.done', () => rt.close());
44+
45+
rt.socket.addEventListener('close', () => console.log('\nConnection closed!'));
46+
}
47+
48+
main();

‎examples/realtime/ws.ts

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { OpenAIRealtimeWS } from 'openai/beta/realtime/ws';
2+
3+
async function main() {
4+
const rt = new OpenAIRealtimeWS({ model: 'gpt-4o-realtime-preview-2024-12-17' });
5+
6+
// access the underlying `ws.WebSocket` instance
7+
rt.socket.on('open', () => {
8+
console.log('Connection opened!');
9+
rt.send({
10+
type: 'session.update',
11+
session: {
12+
modalities: ['foo'] as any,
13+
model: 'gpt-4o-realtime-preview',
14+
},
15+
});
16+
rt.send({
17+
type: 'session.update',
18+
session: {
19+
modalities: ['text'],
20+
model: 'gpt-4o-realtime-preview',
21+
},
22+
});
23+
24+
rt.send({
25+
type: 'conversation.item.create',
26+
item: {
27+
type: 'message',
28+
role: 'user',
29+
content: [{ type: 'input_text', text: 'Say a couple paragraphs!' }],
30+
},
31+
});
32+
33+
rt.send({ type: 'response.create' });
34+
});
35+
36+
rt.on('error', (err) => {
37+
// in a real world scenario this should be logged somewhere as you
38+
// likely want to continue procesing events regardless of any errors
39+
throw err;
40+
});
41+
42+
rt.on('session.created', (event) => {
43+
console.log('session created!', event.session);
44+
console.log();
45+
});
46+
47+
rt.on('response.text.delta', (event) => process.stdout.write(event.delta));
48+
rt.on('response.text.done', () => console.log());
49+
50+
rt.on('response.done', () => rt.close());
51+
52+
rt.socket.on('close', () => console.log('\nConnection closed!'));
53+
}
54+
55+
main();

‎package.json

+6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@swc/core": "^1.3.102",
3737
"@swc/jest": "^0.2.29",
3838
"@types/jest": "^29.4.0",
39+
"@types/ws": "^8.5.13",
3940
"@typescript-eslint/eslint-plugin": "^6.7.0",
4041
"@typescript-eslint/parser": "^6.7.0",
4142
"eslint": "^8.49.0",
@@ -52,6 +53,7 @@
5253
"tsc-multi": "^1.1.0",
5354
"tsconfig-paths": "^4.0.0",
5455
"typescript": "^4.8.2",
56+
"ws": "^8.18.0",
5557
"zod": "^3.23.8"
5658
},
5759
"sideEffects": [
@@ -126,9 +128,13 @@
126128
},
127129
"bin": "./bin/cli",
128130
"peerDependencies": {
131+
"ws": "^8.18.0",
129132
"zod": "^3.23.8"
130133
},
131134
"peerDependenciesMeta": {
135+
"ws": {
136+
"optional": true
137+
},
132138
"zod": {
133139
"optional": true
134140
}

‎src/beta/realtime/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { OpenAIRealtimeError } from './internal-base';

‎src/beta/realtime/internal-base.ts

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { RealtimeClientEvent, RealtimeServerEvent, ErrorEvent } from '../../resources/beta/realtime/realtime';
2+
import { EventEmitter } from '../../lib/EventEmitter';
3+
import { OpenAIError } from '../../error';
4+
5+
export class OpenAIRealtimeError extends OpenAIError {
6+
/**
7+
* The error data that the API sent back in an `error` event.
8+
*/
9+
error?: ErrorEvent.Error | undefined;
10+
11+
/**
12+
* The unique ID of the server event.
13+
*/
14+
event_id?: string | undefined;
15+
16+
constructor(message: string, event: ErrorEvent | null) {
17+
super(message);
18+
19+
this.error = event?.error;
20+
this.event_id = event?.event_id;
21+
}
22+
}
23+
24+
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
25+
26+
type RealtimeEvents = Simplify<
27+
{
28+
event: (event: RealtimeServerEvent) => void;
29+
error: (error: OpenAIRealtimeError) => void;
30+
} & {
31+
[EventType in Exclude<RealtimeServerEvent['type'], 'error'>]: (
32+
event: Extract<RealtimeServerEvent, { type: EventType }>,
33+
) => unknown;
34+
}
35+
>;
36+
37+
export abstract class OpenAIRealtimeEmitter extends EventEmitter<RealtimeEvents> {
38+
/**
39+
* Send an event to the API.
40+
*/
41+
abstract send(event: RealtimeClientEvent): void;
42+
43+
/**
44+
* Close the websocket connection.
45+
*/
46+
abstract close(props?: { code: number; reason: string }): void;
47+
48+
protected _onError(event: null, message: string, cause: any): void;
49+
protected _onError(event: ErrorEvent, message?: string | undefined): void;
50+
protected _onError(event: ErrorEvent | null, message?: string | undefined, cause?: any): void {
51+
message =
52+
event?.error ?
53+
`${event.error.message} code=${event.error.code} param=${event.error.param} type=${event.error.type} event_id=${event.error.event_id}`
54+
: message ?? 'unknown error';
55+
56+
if (!this._hasListener('error')) {
57+
const error = new OpenAIRealtimeError(
58+
message +
59+
`\n\nTo resolve these unhandled rejection errors you should bind an \`error\` callback, e.g. \`rt.on('error', (error) => ...)\` `,
60+
event,
61+
);
62+
// @ts-ignore
63+
error.cause = cause;
64+
Promise.reject(error);
65+
return;
66+
}
67+
68+
const error = new OpenAIRealtimeError(message, event);
69+
// @ts-ignore
70+
error.cause = cause;
71+
72+
this._emit('error', error);
73+
}
74+
}
75+
76+
export function buildRealtimeURL(props: { baseURL: string; model: string }): URL {
77+
const path = '/realtime';
78+
79+
const url = new URL(props.baseURL + (props.baseURL.endsWith('/') ? path.slice(1) : path));
80+
url.protocol = 'wss';
81+
url.searchParams.set('model', props.model);
82+
return url;
83+
}

‎src/beta/realtime/websocket.ts

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { OpenAI } from '../../index';
2+
import { OpenAIError } from '../../error';
3+
import * as Core from '../../core';
4+
import type { RealtimeClientEvent, RealtimeServerEvent } from '../../resources/beta/realtime/realtime';
5+
import { OpenAIRealtimeEmitter, buildRealtimeURL } from './internal-base';
6+
7+
interface MessageEvent {
8+
data: string;
9+
}
10+
11+
type _WebSocket =
12+
typeof globalThis extends (
13+
{
14+
WebSocket: infer ws;
15+
}
16+
) ?
17+
// @ts-ignore
18+
InstanceType<ws>
19+
: any;
20+
21+
export class OpenAIRealtimeWebSocket extends OpenAIRealtimeEmitter {
22+
url: URL;
23+
socket: _WebSocket;
24+
25+
constructor(
26+
props: {
27+
model: string;
28+
dangerouslyAllowBrowser?: boolean;
29+
},
30+
client?: Pick<OpenAI, 'apiKey' | 'baseURL'>,
31+
) {
32+
super();
33+
34+
const dangerouslyAllowBrowser =
35+
props.dangerouslyAllowBrowser ??
36+
(client as any)?._options?.dangerouslyAllowBrowser ??
37+
(client?.apiKey.startsWith('ek_') ? true : null);
38+
39+
if (!dangerouslyAllowBrowser && Core.isRunningInBrowser()) {
40+
throw new OpenAIError(
41+
"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",
42+
);
43+
}
44+
45+
client ??= new OpenAI({ dangerouslyAllowBrowser });
46+
47+
this.url = buildRealtimeURL({ baseURL: client.baseURL, model: props.model });
48+
// @ts-ignore
49+
this.socket = new WebSocket(this.url, [
50+
'realtime',
51+
`openai-insecure-api-key.${client.apiKey}`,
52+
'openai-beta.realtime-v1',
53+
]);
54+
55+
this.socket.addEventListener('message', (websocketEvent: MessageEvent) => {
56+
const event = (() => {
57+
try {
58+
return JSON.parse(websocketEvent.data.toString()) as RealtimeServerEvent;
59+
} catch (err) {
60+
this._onError(null, 'could not parse websocket event', err);
61+
return null;
62+
}
63+
})();
64+
65+
if (event) {
66+
this._emit('event', event);
67+
68+
if (event.type === 'error') {
69+
this._onError(event);
70+
} else {
71+
// @ts-expect-error TS isn't smart enough to get the relationship right here
72+
this._emit(event.type, event);
73+
}
74+
}
75+
});
76+
77+
this.socket.addEventListener('error', (event: any) => {
78+
this._onError(null, event.message, null);
79+
});
80+
}
81+
82+
send(event: RealtimeClientEvent) {
83+
try {
84+
this.socket.send(JSON.stringify(event));
85+
} catch (err) {
86+
this._onError(null, 'could not send data', err);
87+
}
88+
}
89+
90+
close(props?: { code: number; reason: string }) {
91+
try {
92+
this.socket.close(props?.code ?? 1000, props?.reason ?? 'OK');
93+
} catch (err) {
94+
this._onError(null, 'could not close the connection', err);
95+
}
96+
}
97+
}

‎src/beta/realtime/ws.ts

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import WS from 'ws';
2+
import { OpenAI } from '../../index';
3+
import type { RealtimeClientEvent, RealtimeServerEvent } from '../../resources/beta/realtime/realtime';
4+
import { OpenAIRealtimeEmitter, buildRealtimeURL } from './internal-base';
5+
6+
export class OpenAIRealtimeWS extends OpenAIRealtimeEmitter {
7+
url: URL;
8+
socket: WS.WebSocket;
9+
10+
constructor(
11+
props: { model: string; options?: WS.ClientOptions | undefined },
12+
client?: Pick<OpenAI, 'apiKey' | 'baseURL'>,
13+
) {
14+
super();
15+
client ??= new OpenAI();
16+
17+
this.url = buildRealtimeURL({ baseURL: client.baseURL, model: props.model });
18+
this.socket = new WS.WebSocket(this.url, {
19+
...props.options,
20+
headers: {
21+
...props.options?.headers,
22+
Authorization: `Bearer ${client.apiKey}`,
23+
'OpenAI-Beta': 'realtime=v1',
24+
},
25+
});
26+
27+
this.socket.on('message', (wsEvent) => {
28+
const event = (() => {
29+
try {
30+
return JSON.parse(wsEvent.toString()) as RealtimeServerEvent;
31+
} catch (err) {
32+
this._onError(null, 'could not parse websocket event', err);
33+
return null;
34+
}
35+
})();
36+
37+
if (event) {
38+
this._emit('event', event);
39+
40+
if (event.type === 'error') {
41+
this._onError(event);
42+
} else {
43+
// @ts-expect-error TS isn't smart enough to get the relationship right here
44+
this._emit(event.type, event);
45+
}
46+
}
47+
});
48+
49+
this.socket.on('error', (err) => {
50+
this._onError(null, err.message, err);
51+
});
52+
}
53+
54+
send(event: RealtimeClientEvent) {
55+
try {
56+
this.socket.send(JSON.stringify(event));
57+
} catch (err) {
58+
this._onError(null, 'could not send data', err);
59+
}
60+
}
61+
62+
close(props?: { code: number; reason: string }) {
63+
try {
64+
this.socket.close(props?.code ?? 1000, props?.reason ?? 'OK');
65+
} catch (err) {
66+
this._onError(null, 'could not close the connection', err);
67+
}
68+
}
69+
}

‎src/lib/EventEmitter.ts

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
type EventListener<Events, EventType extends keyof Events> = Events[EventType];
2+
3+
type EventListeners<Events, EventType extends keyof Events> = Array<{
4+
listener: EventListener<Events, EventType>;
5+
once?: boolean;
6+
}>;
7+
8+
export type EventParameters<Events, EventType extends keyof Events> = {
9+
[Event in EventType]: EventListener<Events, EventType> extends (...args: infer P) => any ? P : never;
10+
}[EventType];
11+
12+
export class EventEmitter<EventTypes extends Record<string, (...args: any) => any>> {
13+
#listeners: {
14+
[Event in keyof EventTypes]?: EventListeners<EventTypes, Event>;
15+
} = {};
16+
17+
/**
18+
* Adds the listener function to the end of the listeners array for the event.
19+
* No checks are made to see if the listener has already been added. Multiple calls passing
20+
* the same combination of event and listener will result in the listener being added, and
21+
* called, multiple times.
22+
* @returns this, so that calls can be chained
23+
*/
24+
on<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
25+
const listeners: EventListeners<EventTypes, Event> =
26+
this.#listeners[event] || (this.#listeners[event] = []);
27+
listeners.push({ listener });
28+
return this;
29+
}
30+
31+
/**
32+
* Removes the specified listener from the listener array for the event.
33+
* off() will remove, at most, one instance of a listener from the listener array. If any single
34+
* listener has been added multiple times to the listener array for the specified event, then
35+
* off() must be called multiple times to remove each instance.
36+
* @returns this, so that calls can be chained
37+
*/
38+
off<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
39+
const listeners = this.#listeners[event];
40+
if (!listeners) return this;
41+
const index = listeners.findIndex((l) => l.listener === listener);
42+
if (index >= 0) listeners.splice(index, 1);
43+
return this;
44+
}
45+
46+
/**
47+
* Adds a one-time listener function for the event. The next time the event is triggered,
48+
* this listener is removed and then invoked.
49+
* @returns this, so that calls can be chained
50+
*/
51+
once<Event extends keyof EventTypes>(event: Event, listener: EventListener<EventTypes, Event>): this {
52+
const listeners: EventListeners<EventTypes, Event> =
53+
this.#listeners[event] || (this.#listeners[event] = []);
54+
listeners.push({ listener, once: true });
55+
return this;
56+
}
57+
58+
/**
59+
* This is similar to `.once()`, but returns a Promise that resolves the next time
60+
* the event is triggered, instead of calling a listener callback.
61+
* @returns a Promise that resolves the next time given event is triggered,
62+
* or rejects if an error is emitted. (If you request the 'error' event,
63+
* returns a promise that resolves with the error).
64+
*
65+
* Example:
66+
*
67+
* const message = await stream.emitted('message') // rejects if the stream errors
68+
*/
69+
emitted<Event extends keyof EventTypes>(
70+
event: Event,
71+
): Promise<
72+
EventParameters<EventTypes, Event> extends [infer Param] ? Param
73+
: EventParameters<EventTypes, Event> extends [] ? void
74+
: EventParameters<EventTypes, Event>
75+
> {
76+
return new Promise((resolve, reject) => {
77+
// TODO: handle errors
78+
this.once(event, resolve as any);
79+
});
80+
}
81+
82+
protected _emit<Event extends keyof EventTypes>(
83+
this: EventEmitter<EventTypes>,
84+
event: Event,
85+
...args: EventParameters<EventTypes, Event>
86+
) {
87+
const listeners: EventListeners<EventTypes, Event> | undefined = this.#listeners[event];
88+
if (listeners) {
89+
this.#listeners[event] = listeners.filter((l) => !l.once) as any;
90+
listeners.forEach(({ listener }: any) => listener(...(args as any)));
91+
}
92+
}
93+
94+
protected _hasListener(event: keyof EventTypes): boolean {
95+
const listeners = this.#listeners[event];
96+
return listeners && listeners.length > 0;
97+
}
98+
}

‎yarn.lock

+12
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,13 @@
881881
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
882882
integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==
883883

884+
"@types/ws@^8.5.13":
885+
version "8.5.13"
886+
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20"
887+
integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==
888+
dependencies:
889+
"@types/node" "*"
890+
884891
"@types/yargs-parser@*":
885892
version "21.0.3"
886893
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15"
@@ -3472,6 +3479,11 @@ write-file-atomic@^4.0.2:
34723479
imurmurhash "^0.1.4"
34733480
signal-exit "^3.0.7"
34743481

3482+
ws@^8.18.0:
3483+
version "8.18.0"
3484+
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
3485+
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
3486+
34753487
y18n@^5.0.5:
34763488
version "5.0.8"
34773489
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"

0 commit comments

Comments
 (0)
Please sign in to comment.