@urun-sh/openai 0.2.26
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/LICENSE +21 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +174 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 urun contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var U=Object.create;var m=Object.defineProperty;var D=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var $=Object.getPrototypeOf,j=Object.prototype.hasOwnProperty;var H=(n,e)=>{for(var t in e)m(n,t,{get:e[t],enumerable:!0})},_=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of q(e))!j.call(n,o)&&o!==t&&m(n,o,{get:()=>e[o],enumerable:!(r=D(e,o))||r.enumerable});return n};var v=(n,e,t)=>(t=n!=null?U($(n)):{},_(e||!n||!n.__esModule?m(t,"default",{value:n,enumerable:!0}):t,n)),N=n=>_(m({},"__esModule",{value:!0}),n);var G={};H(G,{SUPPORTED_REALTIME_CLIENT_EVENTS:()=>h,SUPPORTED_RESPONSES_EVENTS:()=>S,UrunRealtime:()=>k,UrunResponses:()=>y,isSupportedClientEvent:()=>L,isSupportedResponsesEvent:()=>P,listModels:()=>T,nodeAudioBackend:()=>R,toErrorEvent:()=>d,weriftAudioBackend:()=>A});module.exports=N(G);function b(n,e,t,r){let o=n.response??{},s={request_id:t,consumer_id:r,stream:!0,kind:"chat",messages:e};return typeof o.instructions=="string"&&(s.instructions=o.instructions),Array.isArray(o.modalities)&&(s.modalities=o.modalities),typeof o.temperature=="number"&&(s.temperature=o.temperature),typeof o.max_output_tokens=="number"&&(s.max_output_tokens=o.max_output_tokens),o.tools!==void 0&&(s.tools=o.tools),s}function E(n,e,t){let r={request_id:e,consumer_id:t,stream:!!n.stream,kind:"responses",input:n.input};return n.model&&(r.model=n.model),n.tools!==void 0&&(r.tools=n.tools),typeof n.temperature=="number"&&(r.temperature=n.temperature),typeof n.max_output_tokens=="number"&&(r.max_output_tokens=n.max_output_tokens),r}function d(n){if(n instanceof Error)return{type:"error",error:{type:"urun_error",code:n.name||null,message:n.message}};if(n&&typeof n=="object"&&n.t==="error"){let e=n,t=e.body??{};return{type:"error",error:{type:"urun_error",code:e.code??null,message:t.message??"unknown error"}}}return{type:"error",error:{type:"urun_error",code:null,message:String(n)}}}var W="llm-resp";async function*g(n,e){let t=`${W}:${e}`,r=`resp_${e}`;yield{type:"response.created",response:{id:r,status:"in_progress"}};for await(let o of n.stream(t).messages()){let s=o;if(s.t==="delta")yield{type:"response.output_text.delta",item_id:r,delta:s.delta};else if(s.t==="response"){yield{type:"response.completed",response:{id:r,status:"completed",...s.body}};return}else if(s.t==="error"){yield d(s);return}}}var F="llm",c=class{constructor(e){this.session=e}session;get consumerId(){return this.session.consumerId}write(e){this.session.doc(F).set({requests:{[e.request_id]:{payload:e,consumer_id:e.consumer_id,stream:e.stream}}})}sendResponses(e,t){let r=E(e,t,this.consumerId);return this.write(r),g(this.session,t)}sendResponseCreate(e,t,r){let o=b(e,t,r,this.consumerId);return this.write(o),g(this.session,r)}};var f=class{constructor(e,t){this.backend=e;this.opts=t}backend;opts;source=null;sink=null;outHandlers=new Set;async startOutbound(){return this.source=this.backend.createSource(),this.source.track}appendInputAudio(e){if(!this.source)throw new Error("startOutbound() not called");let t=Buffer.from(e,"base64"),r=Math.floor(t.byteLength/2),o=new Int16Array(r);for(let s=0;s<r;s++)o[s]=t.readInt16LE(s*2);this.source.onData(o)}async startInbound(e){if(e==null)throw new Error('startInbound: no downstream audio track \u2014 the runtime audio producer has not been consumed yet (session.stream("rt-audio-out").track is null)');this.sink=this.backend.createSink(e),this.sink.onframe=t=>{let r=Buffer.from(t.samples.buffer,t.samples.byteOffset,t.samples.byteLength).toString("base64");for(let o of this.outHandlers)o(r)}}onOutputAudio(e){return this.outHandlers.add(e),()=>this.outHandlers.delete(e)}},K=24e3,l=480,V=111;async function Y(){let n=await import("opusscript"),e=n.default??n;return new e(K,1,2048)}async function A(){let n=await import("werift"),e=await Y();return{createSource:()=>{let t=new n.MediaStreamTrack({kind:"audio"}),r=Math.random()*4294967295>>>0,o=Math.random()*65535&65535,s=Math.random()*4294967295>>>0,i=new Int16Array(0);return{track:t,onData:u=>{let a=new Int16Array(i.length+u.length);a.set(i,0),a.set(u,i.length);let p=0;for(;a.length-p>=l;){let w=a.subarray(p,p+l);p+=l;let O=Buffer.from(w.buffer,w.byteOffset,w.byteLength),B=e.encode(O,l);o=o+1&65535,s=s+l>>>0;let M=new n.RtpHeader({version:2,payloadType:V,sequenceNumber:o,timestamp:s,ssrc:r,marker:!1}),C=new n.RtpPacket(M,B);t.writeRtp(C)}i=a.slice(p)}}},createSink:t=>{let r={onframe:null};return t.onReceiveRtp.subscribe(s=>{let i;try{i=e.decode(s.payload)}catch{return}let u=new Int16Array(i.length/2);for(let a=0;a<u.length;a++)u[a]=i.readInt16LE(a*2);r.onframe?.({samples:u})}),r}}}async function R(){let n=await import("@roamhq/wrtc");return{createSource:()=>{let e=new n.nonstandard.RTCAudioSource;return{track:e.createTrack(),onData:r=>e.onData({samples:r,sampleRate:24e3})}},createSink:e=>{let t={onframe:null},r=new n.nonstandard.RTCAudioSink(e);return r.ondata=o=>{t.onframe?.(o)},t}}}var x=0;function z(){return x+=1,`rt_${Date.now().toString(36)}_${x.toString(36)}`}var k=class{constructor(e,t={}){this.session=e;this.opts=t;this.transport=new c(e)}session;opts;transport;handlers=new Set;conversation=[];active=null;audio=null;currentRequestId=null;async enableAudio(){if(!this.opts.audioBackend)throw new Error("audioBackend required for audio");this.audio=new f(this.opts.audioBackend,{sampleRate:24e3});let e=await this.audio.startOutbound();await this.session.stream("rt-audio-in").attach(e),this.audio.onOutputAudio(r=>this.emit({type:"response.audio.delta",delta:r}));let t=await this.awaitDownstreamAudioTrack();await this.audio.startInbound(t)}awaitDownstreamAudioTrack(e=1e4){let t=this.session.stream("rt-audio-out");return t.track!=null?Promise.resolve(t.track):new Promise((r,o)=>{let s,i=setTimeout(()=>{s?.(),o(new Error(`enableAudio: no downstream audio track within ${e}ms (runtime produced no voice OUT / SFU audio consumer never attached)`))},e),u=a=>{a!=null&&(clearTimeout(i),s?.(),r(a))};s=t.on?.("track",u),t.track!=null&&u(t.track)})}on(e,t){return this.handlers.add(t),()=>this.handlers.delete(t)}emit(e){for(let t of this.handlers)t(e)}send(e){switch(e.type){case"conversation.item.create":{let t=(e.item?.content??[]).map(r=>r.text??"").join("");this.conversation.push({role:e.item?.role??"user",content:t});return}case"response.create":{let t=z();this.currentRequestId=t;let r=this.transport.sendResponseCreate({type:"response.create",response:e.response},this.conversation,t);this.active=(async()=>{try{for await(let o of r)this.emit(o)}catch(o){this.emit(d(o))}})();return}case"input_audio_buffer.append":this.audio?.appendInputAudio(e.audio);return;case"input_audio_buffer.commit":case"input_audio_buffer.clear":return;case"response.cancel":return;default:return}}async drain(){this.active&&await this.active}};var I=0;function X(){return I+=1,`req_${Date.now().toString(36)}_${I.toString(36)}`}var y=class{transport;constructor(e){this.transport=new c(e)}responses={create:async e=>{let t=X(),r=this.transport.sendResponses(e,t);return Object.assign((async function*(){yield*r})(),{requestId:t})}}};async function T(n){let t=await(n.fetchImpl??fetch)(`${n.catalogUrl}/model_catalog?select=model_id,variant`,{headers:{apikey:n.anonKey,Authorization:`Bearer ${n.anonKey}`}});if(!t.ok)throw new Error(`model_catalog fetch failed: ${t.status}`);return{object:"list",data:(await t.json()).map(o=>({id:`${o.model_id}:${o.variant}`,object:"model",created:0,owned_by:"urun"}))}}var h=["session.update","conversation.item.create","response.create","response.cancel","input_audio_buffer.append","input_audio_buffer.commit","input_audio_buffer.clear"],S=["response.created","response.output_text.delta","response.output_item.added","response.function_call_arguments.delta","response.image_generation_call.partial_image","response.completed","error"];function L(n){return h.includes(n)}function P(n){return S.includes(n)}0&&(module.exports={SUPPORTED_REALTIME_CLIENT_EVENTS,SUPPORTED_RESPONSES_EVENTS,UrunRealtime,UrunResponses,isSupportedClientEvent,isSupportedResponsesEvent,listModels,nodeAudioBackend,toErrorEvent,weriftAudioBackend});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export { RealtimeClientEvent, RealtimeServerEvent } from 'openai/resources/beta/realtime/realtime';
|
|
2
|
+
export { ResponseStreamEvent } from 'openai/resources/responses/responses';
|
|
3
|
+
|
|
4
|
+
/** The narrow subset of @urun-sh/core's Session the SDK depends on. */
|
|
5
|
+
interface UrunDocLike {
|
|
6
|
+
set(patch: Record<string, unknown>): void;
|
|
7
|
+
get(path?: string, defaultValue?: unknown): unknown;
|
|
8
|
+
on(event: 'change', handler: (snapshot: Record<string, unknown>) => void): () => void;
|
|
9
|
+
}
|
|
10
|
+
interface UrunStreamLike {
|
|
11
|
+
messages(): AsyncIterable<unknown>;
|
|
12
|
+
attach(track: unknown): Promise<void>;
|
|
13
|
+
detach(): Promise<void>;
|
|
14
|
+
readonly track: unknown;
|
|
15
|
+
/**
|
|
16
|
+
* Subscribe to the named media track (de)appearing. The downstream audio track
|
|
17
|
+
* is consumed ASYNCHRONOUSLY after the WebRTC handshake, so `track` may be null
|
|
18
|
+
* at `enableAudio()` time; this fires with the real `MediaStreamTrack` the
|
|
19
|
+
* moment the consumer attaches (core `SessionStream.on('track')`). Optional so
|
|
20
|
+
* non-media test doubles need not implement it.
|
|
21
|
+
*/
|
|
22
|
+
on?(event: 'track', handler: (track: unknown) => void): () => void;
|
|
23
|
+
}
|
|
24
|
+
interface UrunSessionLike {
|
|
25
|
+
/** Per-leg consumer identity (core: transport.consumerId). */
|
|
26
|
+
readonly consumerId: string;
|
|
27
|
+
doc(key: string): UrunDocLike;
|
|
28
|
+
stream(name: string): UrunStreamLike;
|
|
29
|
+
close?(): Promise<void> | void;
|
|
30
|
+
}
|
|
31
|
+
/** Lane message shape emitted by the serve runtime (core session.ts:83). */
|
|
32
|
+
type LaneMessage = {
|
|
33
|
+
t: 'delta';
|
|
34
|
+
request_id?: string;
|
|
35
|
+
delta: string;
|
|
36
|
+
} | {
|
|
37
|
+
t: 'response';
|
|
38
|
+
request_id?: string;
|
|
39
|
+
body: unknown;
|
|
40
|
+
} | {
|
|
41
|
+
t: 'error';
|
|
42
|
+
request_id?: string;
|
|
43
|
+
body: unknown;
|
|
44
|
+
code?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Pluggable audio backend so tests don't need real WebRTC. In production, `nodeAudioBackend()` wraps @roamhq/wrtc. */
|
|
48
|
+
interface AudioSource {
|
|
49
|
+
track: unknown;
|
|
50
|
+
onData(frame: Int16Array): void;
|
|
51
|
+
}
|
|
52
|
+
interface AudioSink {
|
|
53
|
+
onframe: ((frame: {
|
|
54
|
+
samples: Int16Array;
|
|
55
|
+
}) => void) | null;
|
|
56
|
+
}
|
|
57
|
+
interface AudioBackend {
|
|
58
|
+
createSource(): AudioSource;
|
|
59
|
+
/**
|
|
60
|
+
* Build a sink that pulls PCM16 frames OFF the runtime's downstream audio
|
|
61
|
+
* track (`remoteTrack`, the real Opus `MediaStreamTrack` the SFU now produces
|
|
62
|
+
* — see session-server-ts audio ingest). The production backend wires a real
|
|
63
|
+
* `RTCAudioSink(remoteTrack)`; the sink's `onframe` is invoked per decoded
|
|
64
|
+
* 10ms frame. `remoteTrack` is REQUIRED — a missing track is a wiring bug, not
|
|
65
|
+
* a silent no-op (a dead sink is exactly the placeholder this replaces).
|
|
66
|
+
*/
|
|
67
|
+
createSink(remoteTrack: unknown): AudioSink;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Production backend for the werift transport (@urun-sh/core). Replaces the
|
|
71
|
+
* wrtc backend, whose `RTCAudioSink`/`RTCAudioSource` are incompatible with
|
|
72
|
+
* werift's read-only-`id` `MediaStreamTrack`.
|
|
73
|
+
*/
|
|
74
|
+
declare function weriftAudioBackend(): Promise<AudioBackend>;
|
|
75
|
+
/**
|
|
76
|
+
* @deprecated wrtc backend — INCOMPATIBLE with @urun-sh/core's werift transport
|
|
77
|
+
* (its `RTCAudioSink`/`RTCAudioSource` set the foreign track's read-only `id`).
|
|
78
|
+
* Use {@link weriftAudioBackend}. Kept for any non-werift transport.
|
|
79
|
+
*/
|
|
80
|
+
declare function nodeAudioBackend(): Promise<AudioBackend>;
|
|
81
|
+
|
|
82
|
+
type ServerEventHandler = (ev: unknown) => void;
|
|
83
|
+
interface RealtimeClientEventLike {
|
|
84
|
+
type: string;
|
|
85
|
+
item?: {
|
|
86
|
+
role: string;
|
|
87
|
+
content: Array<{
|
|
88
|
+
type: string;
|
|
89
|
+
text?: string;
|
|
90
|
+
}>;
|
|
91
|
+
};
|
|
92
|
+
response?: Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
declare class UrunRealtime {
|
|
95
|
+
private readonly session;
|
|
96
|
+
private readonly opts;
|
|
97
|
+
private readonly transport;
|
|
98
|
+
private readonly handlers;
|
|
99
|
+
private conversation;
|
|
100
|
+
private active;
|
|
101
|
+
private audio;
|
|
102
|
+
currentRequestId: string | null;
|
|
103
|
+
constructor(session: UrunSessionLike, opts?: {
|
|
104
|
+
audioBackend?: AudioBackend;
|
|
105
|
+
});
|
|
106
|
+
enableAudio(): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the downstream audio `MediaStreamTrack`: immediately if already
|
|
109
|
+
* consumed, otherwise on the first `track` event from the `rt-audio-out`
|
|
110
|
+
* stream. Times out (default 10s) so `enableAudio` cannot hang forever if the
|
|
111
|
+
* runtime never produces voice OUT (e.g. a video-only app).
|
|
112
|
+
*/
|
|
113
|
+
private awaitDownstreamAudioTrack;
|
|
114
|
+
on(_event: 'server_event', handler: ServerEventHandler): () => void;
|
|
115
|
+
private emit;
|
|
116
|
+
/** OpenAI Realtime client-event entrypoint. */
|
|
117
|
+
send(ev: RealtimeClientEventLike): void;
|
|
118
|
+
/** Await the in-flight response turn (test + flush helper). */
|
|
119
|
+
drain(): Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface ResponsesStream extends AsyncIterable<unknown> {
|
|
123
|
+
readonly requestId: string;
|
|
124
|
+
}
|
|
125
|
+
declare class UrunResponses {
|
|
126
|
+
private readonly transport;
|
|
127
|
+
constructor(session: UrunSessionLike);
|
|
128
|
+
readonly responses: {
|
|
129
|
+
create: (params: {
|
|
130
|
+
model?: string;
|
|
131
|
+
input: unknown;
|
|
132
|
+
stream?: boolean;
|
|
133
|
+
tools?: unknown;
|
|
134
|
+
temperature?: number;
|
|
135
|
+
max_output_tokens?: number;
|
|
136
|
+
}) => Promise<ResponsesStream>;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface ModelObject {
|
|
141
|
+
id: string;
|
|
142
|
+
object: 'model';
|
|
143
|
+
created: number;
|
|
144
|
+
owned_by: string;
|
|
145
|
+
}
|
|
146
|
+
interface ModelList {
|
|
147
|
+
object: 'list';
|
|
148
|
+
data: ModelObject[];
|
|
149
|
+
}
|
|
150
|
+
declare function listModels(opts: {
|
|
151
|
+
catalogUrl: string;
|
|
152
|
+
anonKey: string;
|
|
153
|
+
fetchImpl?: typeof fetch;
|
|
154
|
+
}): Promise<ModelList>;
|
|
155
|
+
|
|
156
|
+
interface OpenAIErrorEvent {
|
|
157
|
+
type: 'error';
|
|
158
|
+
error: {
|
|
159
|
+
type: string;
|
|
160
|
+
code: string | null;
|
|
161
|
+
message: string;
|
|
162
|
+
param?: string | null;
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
declare function toErrorEvent(fault: LaneMessage | Error | unknown): OpenAIErrorEvent;
|
|
166
|
+
|
|
167
|
+
/** The Realtime client-events the SDK maps in v1. */
|
|
168
|
+
declare const SUPPORTED_REALTIME_CLIENT_EVENTS: readonly ["session.update", "conversation.item.create", "response.create", "response.cancel", "input_audio_buffer.append", "input_audio_buffer.commit", "input_audio_buffer.clear"];
|
|
169
|
+
/** The Responses streaming events the SDK emits in v1. */
|
|
170
|
+
declare const SUPPORTED_RESPONSES_EVENTS: readonly ["response.created", "response.output_text.delta", "response.output_item.added", "response.function_call_arguments.delta", "response.image_generation_call.partial_image", "response.completed", "error"];
|
|
171
|
+
declare function isSupportedClientEvent(type: string): boolean;
|
|
172
|
+
declare function isSupportedResponsesEvent(type: string): boolean;
|
|
173
|
+
|
|
174
|
+
export { type AudioBackend, type AudioSink, type AudioSource, type LaneMessage, type ModelList, type ModelObject, type OpenAIErrorEvent, SUPPORTED_REALTIME_CLIENT_EVENTS, SUPPORTED_RESPONSES_EVENTS, UrunRealtime, UrunResponses, type UrunSessionLike, isSupportedClientEvent, isSupportedResponsesEvent, listModels, nodeAudioBackend, toErrorEvent, weriftAudioBackend };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
export { RealtimeClientEvent, RealtimeServerEvent } from 'openai/resources/beta/realtime/realtime';
|
|
2
|
+
export { ResponseStreamEvent } from 'openai/resources/responses/responses';
|
|
3
|
+
|
|
4
|
+
interface UrunDocLike {
|
|
5
|
+
set(patch: Record<string, unknown>): void;
|
|
6
|
+
get(path?: string, defaultValue?: unknown): unknown;
|
|
7
|
+
on(event: 'change', handler: (snapshot: Record<string, unknown>) => void): () => void;
|
|
8
|
+
}
|
|
9
|
+
interface UrunStreamLike {
|
|
10
|
+
messages(): AsyncIterable<unknown>;
|
|
11
|
+
attach(track: unknown): Promise<void>;
|
|
12
|
+
detach(): Promise<void>;
|
|
13
|
+
readonly track: unknown;
|
|
14
|
+
|
|
15
|
+
on?(event: 'track', handler: (track: unknown) => void): () => void;
|
|
16
|
+
}
|
|
17
|
+
interface UrunSessionLike {
|
|
18
|
+
|
|
19
|
+
readonly consumerId: string;
|
|
20
|
+
doc(key: string): UrunDocLike;
|
|
21
|
+
stream(name: string): UrunStreamLike;
|
|
22
|
+
close?(): Promise<void> | void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type LaneMessage = {
|
|
26
|
+
t: 'delta';
|
|
27
|
+
request_id?: string;
|
|
28
|
+
delta: string;
|
|
29
|
+
} | {
|
|
30
|
+
t: 'response';
|
|
31
|
+
request_id?: string;
|
|
32
|
+
body: unknown;
|
|
33
|
+
} | {
|
|
34
|
+
t: 'error';
|
|
35
|
+
request_id?: string;
|
|
36
|
+
body: unknown;
|
|
37
|
+
code?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
interface AudioSource {
|
|
41
|
+
track: unknown;
|
|
42
|
+
onData(frame: Int16Array): void;
|
|
43
|
+
}
|
|
44
|
+
interface AudioSink {
|
|
45
|
+
onframe: ((frame: {
|
|
46
|
+
samples: Int16Array;
|
|
47
|
+
}) => void) | null;
|
|
48
|
+
}
|
|
49
|
+
interface AudioBackend {
|
|
50
|
+
createSource(): AudioSource;
|
|
51
|
+
|
|
52
|
+
createSink(remoteTrack: unknown): AudioSink;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
declare function weriftAudioBackend(): Promise<AudioBackend>;
|
|
56
|
+
|
|
57
|
+
declare function nodeAudioBackend(): Promise<AudioBackend>;
|
|
58
|
+
|
|
59
|
+
type ServerEventHandler = (ev: unknown) => void;
|
|
60
|
+
interface RealtimeClientEventLike {
|
|
61
|
+
type: string;
|
|
62
|
+
item?: {
|
|
63
|
+
role: string;
|
|
64
|
+
content: Array<{
|
|
65
|
+
type: string;
|
|
66
|
+
text?: string;
|
|
67
|
+
}>;
|
|
68
|
+
};
|
|
69
|
+
response?: Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
declare class UrunRealtime {
|
|
72
|
+
private readonly session;
|
|
73
|
+
private readonly opts;
|
|
74
|
+
private readonly transport;
|
|
75
|
+
private readonly handlers;
|
|
76
|
+
private conversation;
|
|
77
|
+
private active;
|
|
78
|
+
private audio;
|
|
79
|
+
currentRequestId: string | null;
|
|
80
|
+
constructor(session: UrunSessionLike, opts?: {
|
|
81
|
+
audioBackend?: AudioBackend;
|
|
82
|
+
});
|
|
83
|
+
enableAudio(): Promise<void>;
|
|
84
|
+
|
|
85
|
+
private awaitDownstreamAudioTrack;
|
|
86
|
+
on(_event: 'server_event', handler: ServerEventHandler): () => void;
|
|
87
|
+
private emit;
|
|
88
|
+
|
|
89
|
+
send(ev: RealtimeClientEventLike): void;
|
|
90
|
+
|
|
91
|
+
drain(): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface ResponsesStream extends AsyncIterable<unknown> {
|
|
95
|
+
readonly requestId: string;
|
|
96
|
+
}
|
|
97
|
+
declare class UrunResponses {
|
|
98
|
+
private readonly transport;
|
|
99
|
+
constructor(session: UrunSessionLike);
|
|
100
|
+
readonly responses: {
|
|
101
|
+
create: (params: {
|
|
102
|
+
model?: string;
|
|
103
|
+
input: unknown;
|
|
104
|
+
stream?: boolean;
|
|
105
|
+
tools?: unknown;
|
|
106
|
+
temperature?: number;
|
|
107
|
+
max_output_tokens?: number;
|
|
108
|
+
}) => Promise<ResponsesStream>;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface ModelObject {
|
|
113
|
+
id: string;
|
|
114
|
+
object: 'model';
|
|
115
|
+
created: number;
|
|
116
|
+
owned_by: string;
|
|
117
|
+
}
|
|
118
|
+
interface ModelList {
|
|
119
|
+
object: 'list';
|
|
120
|
+
data: ModelObject[];
|
|
121
|
+
}
|
|
122
|
+
declare function listModels(opts: {
|
|
123
|
+
catalogUrl: string;
|
|
124
|
+
anonKey: string;
|
|
125
|
+
fetchImpl?: typeof fetch;
|
|
126
|
+
}): Promise<ModelList>;
|
|
127
|
+
|
|
128
|
+
interface OpenAIErrorEvent {
|
|
129
|
+
type: 'error';
|
|
130
|
+
error: {
|
|
131
|
+
type: string;
|
|
132
|
+
code: string | null;
|
|
133
|
+
message: string;
|
|
134
|
+
param?: string | null;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
declare function toErrorEvent(fault: LaneMessage | Error | unknown): OpenAIErrorEvent;
|
|
138
|
+
|
|
139
|
+
declare const SUPPORTED_REALTIME_CLIENT_EVENTS: readonly ["session.update", "conversation.item.create", "response.create", "response.cancel", "input_audio_buffer.append", "input_audio_buffer.commit", "input_audio_buffer.clear"];
|
|
140
|
+
|
|
141
|
+
declare const SUPPORTED_RESPONSES_EVENTS: readonly ["response.created", "response.output_text.delta", "response.output_item.added", "response.function_call_arguments.delta", "response.image_generation_call.partial_image", "response.completed", "error"];
|
|
142
|
+
declare function isSupportedClientEvent(type: string): boolean;
|
|
143
|
+
declare function isSupportedResponsesEvent(type: string): boolean;
|
|
144
|
+
|
|
145
|
+
export { type AudioBackend, type AudioSink, type AudioSource, type LaneMessage, type ModelList, type ModelObject, type OpenAIErrorEvent, SUPPORTED_REALTIME_CLIENT_EVENTS, SUPPORTED_RESPONSES_EVENTS, UrunRealtime, UrunResponses, type UrunSessionLike, isSupportedClientEvent, isSupportedResponsesEvent, listModels, nodeAudioBackend, toErrorEvent, weriftAudioBackend };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function v(n,e,t,r){let o=n.response??{},s={request_id:t,consumer_id:r,stream:!0,kind:"chat",messages:e};return typeof o.instructions=="string"&&(s.instructions=o.instructions),Array.isArray(o.modalities)&&(s.modalities=o.modalities),typeof o.temperature=="number"&&(s.temperature=o.temperature),typeof o.max_output_tokens=="number"&&(s.max_output_tokens=o.max_output_tokens),o.tools!==void 0&&(s.tools=o.tools),s}function g(n,e,t){let r={request_id:e,consumer_id:t,stream:!!n.stream,kind:"responses",input:n.input};return n.model&&(r.model=n.model),n.tools!==void 0&&(r.tools=n.tools),typeof n.temperature=="number"&&(r.temperature=n.temperature),typeof n.max_output_tokens=="number"&&(r.max_output_tokens=n.max_output_tokens),r}function p(n){if(n instanceof Error)return{type:"error",error:{type:"urun_error",code:n.name||null,message:n.message}};if(n&&typeof n=="object"&&n.t==="error"){let e=n,t=e.body??{};return{type:"error",error:{type:"urun_error",code:e.code??null,message:t.message??"unknown error"}}}return{type:"error",error:{type:"urun_error",code:null,message:String(n)}}}var I="llm-resp";async function*k(n,e){let t=`${I}:${e}`,r=`resp_${e}`;yield{type:"response.created",response:{id:r,status:"in_progress"}};for await(let o of n.stream(t).messages()){let s=o;if(s.t==="delta")yield{type:"response.output_text.delta",item_id:r,delta:s.delta};else if(s.t==="response"){yield{type:"response.completed",response:{id:r,status:"completed",...s.body}};return}else if(s.t==="error"){yield p(s);return}}}var T="llm",d=class{constructor(e){this.session=e}session;get consumerId(){return this.session.consumerId}write(e){this.session.doc(T).set({requests:{[e.request_id]:{payload:e,consumer_id:e.consumer_id,stream:e.stream}}})}sendResponses(e,t){let r=g(e,t,this.consumerId);return this.write(r),k(this.session,t)}sendResponseCreate(e,t,r){let o=v(e,t,r,this.consumerId);return this.write(o),k(this.session,r)}};var m=class{constructor(e,t){this.backend=e;this.opts=t}backend;opts;source=null;sink=null;outHandlers=new Set;async startOutbound(){return this.source=this.backend.createSource(),this.source.track}appendInputAudio(e){if(!this.source)throw new Error("startOutbound() not called");let t=Buffer.from(e,"base64"),r=Math.floor(t.byteLength/2),o=new Int16Array(r);for(let s=0;s<r;s++)o[s]=t.readInt16LE(s*2);this.source.onData(o)}async startInbound(e){if(e==null)throw new Error('startInbound: no downstream audio track \u2014 the runtime audio producer has not been consumed yet (session.stream("rt-audio-out").track is null)');this.sink=this.backend.createSink(e),this.sink.onframe=t=>{let r=Buffer.from(t.samples.buffer,t.samples.byteOffset,t.samples.byteLength).toString("base64");for(let o of this.outHandlers)o(r)}}onOutputAudio(e){return this.outHandlers.add(e),()=>this.outHandlers.delete(e)}},L=24e3,l=480,P=111;async function O(){let n=await import("opusscript"),e=n.default??n;return new e(L,1,2048)}async function B(){let n=await import("werift"),e=await O();return{createSource:()=>{let t=new n.MediaStreamTrack({kind:"audio"}),r=Math.random()*4294967295>>>0,o=Math.random()*65535&65535,s=Math.random()*4294967295>>>0,i=new Int16Array(0);return{track:t,onData:u=>{let a=new Int16Array(i.length+u.length);a.set(i,0),a.set(u,i.length);let c=0;for(;a.length-c>=l;){let f=a.subarray(c,c+l);c+=l;let E=Buffer.from(f.buffer,f.byteOffset,f.byteLength),A=e.encode(E,l);o=o+1&65535,s=s+l>>>0;let R=new n.RtpHeader({version:2,payloadType:P,sequenceNumber:o,timestamp:s,ssrc:r,marker:!1}),x=new n.RtpPacket(R,A);t.writeRtp(x)}i=a.slice(c)}}},createSink:t=>{let r={onframe:null};return t.onReceiveRtp.subscribe(s=>{let i;try{i=e.decode(s.payload)}catch{return}let u=new Int16Array(i.length/2);for(let a=0;a<u.length;a++)u[a]=i.readInt16LE(a*2);r.onframe?.({samples:u})}),r}}}async function M(){let n=await import("@roamhq/wrtc");return{createSource:()=>{let e=new n.nonstandard.RTCAudioSource;return{track:e.createTrack(),onData:r=>e.onData({samples:r,sampleRate:24e3})}},createSink:e=>{let t={onframe:null},r=new n.nonstandard.RTCAudioSink(e);return r.ondata=o=>{t.onframe?.(o)},t}}}var h=0;function C(){return h+=1,`rt_${Date.now().toString(36)}_${h.toString(36)}`}var y=class{constructor(e,t={}){this.session=e;this.opts=t;this.transport=new d(e)}session;opts;transport;handlers=new Set;conversation=[];active=null;audio=null;currentRequestId=null;async enableAudio(){if(!this.opts.audioBackend)throw new Error("audioBackend required for audio");this.audio=new m(this.opts.audioBackend,{sampleRate:24e3});let e=await this.audio.startOutbound();await this.session.stream("rt-audio-in").attach(e),this.audio.onOutputAudio(r=>this.emit({type:"response.audio.delta",delta:r}));let t=await this.awaitDownstreamAudioTrack();await this.audio.startInbound(t)}awaitDownstreamAudioTrack(e=1e4){let t=this.session.stream("rt-audio-out");return t.track!=null?Promise.resolve(t.track):new Promise((r,o)=>{let s,i=setTimeout(()=>{s?.(),o(new Error(`enableAudio: no downstream audio track within ${e}ms (runtime produced no voice OUT / SFU audio consumer never attached)`))},e),u=a=>{a!=null&&(clearTimeout(i),s?.(),r(a))};s=t.on?.("track",u),t.track!=null&&u(t.track)})}on(e,t){return this.handlers.add(t),()=>this.handlers.delete(t)}emit(e){for(let t of this.handlers)t(e)}send(e){switch(e.type){case"conversation.item.create":{let t=(e.item?.content??[]).map(r=>r.text??"").join("");this.conversation.push({role:e.item?.role??"user",content:t});return}case"response.create":{let t=C();this.currentRequestId=t;let r=this.transport.sendResponseCreate({type:"response.create",response:e.response},this.conversation,t);this.active=(async()=>{try{for await(let o of r)this.emit(o)}catch(o){this.emit(p(o))}})();return}case"input_audio_buffer.append":this.audio?.appendInputAudio(e.audio);return;case"input_audio_buffer.commit":case"input_audio_buffer.clear":return;case"response.cancel":return;default:return}}async drain(){this.active&&await this.active}};var S=0;function U(){return S+=1,`req_${Date.now().toString(36)}_${S.toString(36)}`}var w=class{transport;constructor(e){this.transport=new d(e)}responses={create:async e=>{let t=U(),r=this.transport.sendResponses(e,t);return Object.assign((async function*(){yield*r})(),{requestId:t})}}};async function D(n){let t=await(n.fetchImpl??fetch)(`${n.catalogUrl}/model_catalog?select=model_id,variant`,{headers:{apikey:n.anonKey,Authorization:`Bearer ${n.anonKey}`}});if(!t.ok)throw new Error(`model_catalog fetch failed: ${t.status}`);return{object:"list",data:(await t.json()).map(o=>({id:`${o.model_id}:${o.variant}`,object:"model",created:0,owned_by:"urun"}))}}var _=["session.update","conversation.item.create","response.create","response.cancel","input_audio_buffer.append","input_audio_buffer.commit","input_audio_buffer.clear"],b=["response.created","response.output_text.delta","response.output_item.added","response.function_call_arguments.delta","response.image_generation_call.partial_image","response.completed","error"];function q(n){return _.includes(n)}function $(n){return b.includes(n)}export{_ as SUPPORTED_REALTIME_CLIENT_EVENTS,b as SUPPORTED_RESPONSES_EVENTS,y as UrunRealtime,w as UrunResponses,q as isSupportedClientEvent,$ as isSupportedResponsesEvent,D as listModels,M as nodeAudioBackend,p as toErrorEvent,B as weriftAudioBackend};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@urun-sh/openai",
|
|
3
|
+
"version": "0.2.26",
|
|
4
|
+
"description": "OpenAI-compatible Realtime + Responses SDK over uRun session primitives (not websockets).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@urun-sh/core": "^0.2.26"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"openai": "4.104.0"
|
|
30
|
+
},
|
|
31
|
+
"optionalDependencies": {
|
|
32
|
+
"@roamhq/wrtc": "^0.8.0",
|
|
33
|
+
"werift": "^0.23.0",
|
|
34
|
+
"opusscript": "^0.1.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"tsup": "^8.5.0",
|
|
38
|
+
"vitest": "^3.0.0",
|
|
39
|
+
"werift": "^0.23.0",
|
|
40
|
+
"opusscript": "^0.1.1",
|
|
41
|
+
"happy-dom": "^20.9.0",
|
|
42
|
+
"typescript": "^5.3.0",
|
|
43
|
+
"@types/node": "^24.0.0",
|
|
44
|
+
"@urun-sh/core": "0.2.26"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup && node ../../scripts/sanitize-dist.mjs .",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"dev": "tsup --watch"
|
|
53
|
+
}
|
|
54
|
+
}
|