@ai-ecoverse/pi-audio 1.7.0 → 1.9.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/README.md +2 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +2 -2
- package/dist/openai-realtime/index.js +58 -0
- package/dist/openai-realtime/index.js.map +7 -0
- package/dist/openai-realtime/session.js +494 -0
- package/dist/openai-realtime/session.js.map +7 -0
- package/dist/openai-realtime/socket.js +56 -0
- package/dist/openai-realtime/socket.js.map +7 -0
- package/dist/queue.js +46 -0
- package/dist/queue.js.map +7 -0
- package/dist/spoken.js +197 -0
- package/dist/spoken.js.map +7 -0
- package/dist/turns.js +347 -0
- package/dist/turns.js.map +7 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/openai-realtime/index.d.ts +7 -0
- package/dist/types/openai-realtime/session.d.ts +15 -0
- package/dist/types/openai-realtime/socket.d.ts +25 -0
- package/dist/types/queue.d.ts +6 -0
- package/dist/types/spoken.d.ts +57 -0
- package/dist/types/turns.d.ts +107 -0
- package/dist/types/xai/index.d.ts +4 -0
- package/dist/xai/index.js +51 -0
- package/dist/xai/index.js.map +7 -0
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -29,6 +29,8 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
|
|
|
29
29
|
|
|
30
30
|
`planDelivery` reads `RealtimeModel.delivery`. An `unsupported` mode throws `VoiceRequestError`. `when_idle` with `approximate` support waits while the model is producing output and runs on `response_done`, `interrupt`, or `close`. Native `when_idle` is sent immediately so the server can schedule it. `planTruncation` adds `{ itemId, audioEndMs }` only when `truncation` is `client` and `interrupt` was given what the listener heard.
|
|
31
31
|
|
|
32
|
+
`@ai-ecoverse/pi-audio/openai-realtime` registers `gpt-realtime-2` and `gpt-realtime-2.1`. `@ai-ecoverse/pi-audio/xai` registers `grok-voice-latest`, an OpenAI Realtime dialect: input transcripts arrive as cumulative `updated` snapshots, `inject` speaks with `force_message` (no `response.create`), and `session.update` opts into `resumption` and an optional `replace` map. Reconnect with `resumeHandle`. Output audio is tagged with the transcript range spoken since the previous delta. Both adapters take a recorded `wire` in tests and never call a provider. WebRTC uses that same event protocol on a caller-supplied data-channel wire.
|
|
33
|
+
|
|
32
34
|
`RealtimeLedger` meters one session. Input seconds come from `appendAudio`. Output seconds come from each `output_audio` event: PCM, mulaw, and alaw from the byte length, and a compressed encoding from character alignment. `sendText` and each delivered `inject` add one text message. Tool results do not. `close()` prices those meters with `priceAudio`. Sum that usage once. `response_done.usage` is the adapter's per-response figure, not a second copy of the session total.
|
|
33
35
|
|
|
34
36
|
`talkerTools` is `ask_agent` and `read`. pi-audio defines `askAgent` (a pi `Tool` whose parameters are `request` and optional `context`) and the `Thinker` interface. The host implements `Thinker` and supplies `read`. `bindThinker` runs direct tools with `directTimeoutMs` and truncates their text to `directMaxChars`. On a blocking `ask_agent` it answers with `ack` immediately and injects the result later. On an async call it `respond`s with the result and `options.result`. Progress is injected with `options.progress`. `tool_call_cancelled` aborts the thinker. The binding also yields `thinker_progress` / `thinker_result` captions and `thinking` / `tool` / `idle` phases.
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { SpeechEventStream } from "./speech.js";
|
|
26
26
|
import { askAgent, bindThinker, talkerTools } from "./talker.js";
|
|
27
27
|
import { TranscriptionEventStream } from "./transcription.js";
|
|
28
|
+
import { voiceTurns } from "./turns.js";
|
|
28
29
|
import { addAudioCounters, priceAudio } from "./usage.js";
|
|
29
30
|
import { energyVad } from "./vad.js";
|
|
30
31
|
export {
|
|
@@ -56,6 +57,7 @@ export {
|
|
|
56
57
|
resolveVoiceAuth,
|
|
57
58
|
speak,
|
|
58
59
|
talkerTools,
|
|
59
|
-
transcribe
|
|
60
|
+
transcribe,
|
|
61
|
+
voiceTurns
|
|
60
62
|
};
|
|
61
63
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport { VoiceRequestError } from './errors.ts';\nexport type {\n Delivery,\n DeliverySupport,\n KnownRealtimeApi,\n KnownSpeechApi,\n KnownTranscriptionApi,\n RealtimeApi,\n RealtimeModel,\n SpeechApi,\n SpeechModel,\n TranscriptionApi,\n TranscriptionModel,\n VoiceInfo,\n VoiceModel,\n VoiceModelType,\n} from './models.ts';\nexport {\n getVoiceModel,\n listVoiceModels,\n registerVoiceModel,\n} from './models.ts';\nexport type {\n DeliveryPlan,\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeSocket,\n RealtimeToken,\n TruncationPlan,\n} from './realtime.ts';\nexport { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from './realtime.ts';\nexport {\n connectRealtime,\n listen,\n registerRealtimeApiProvider,\n registerSpeechApiProvider,\n registerTranscriptionApiProvider,\n speak,\n transcribe,\n} from './registry.ts';\nexport type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';\nexport { SpeechEventStream } from './speech.ts';\nexport type {\n BindThinkerOptions,\n BoundThinker,\n Thinker,\n ThinkerCaption,\n ThinkerPhase,\n ThinkerUpdate,\n} from './talker.ts';\nexport { askAgent, bindThinker, talkerTools } from './talker.ts';\nexport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEvent,\n TranscriptSegment,\n} from './transcription.ts';\nexport { TranscriptionEventStream } from './transcription.ts';\nexport type {\n AudioCost,\n AudioCounters,\n AudioUsage,\n RealtimeAudioCost,\n SpeechAudioCost,\n TranscriptionAudioCost,\n} from './usage.ts';\nexport { addAudioCounters, priceAudio } from './usage.ts';\nexport type { VadHook, VadUpdate } from './vad.ts';\nexport { energyVad } from './vad.ts';\n"],
|
|
5
|
-
"mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAiBlC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAYP,SAAS,cAAc,gBAAgB,gBAAgB,sBAAsB;AAC7E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport { VoiceRequestError } from './errors.ts';\nexport type {\n Delivery,\n DeliverySupport,\n KnownRealtimeApi,\n KnownSpeechApi,\n KnownTranscriptionApi,\n RealtimeApi,\n RealtimeModel,\n SpeechApi,\n SpeechModel,\n TranscriptionApi,\n TranscriptionModel,\n VoiceInfo,\n VoiceModel,\n VoiceModelType,\n} from './models.ts';\nexport {\n getVoiceModel,\n listVoiceModels,\n registerVoiceModel,\n} from './models.ts';\nexport type {\n DeliveryPlan,\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeSocket,\n RealtimeToken,\n TruncationPlan,\n} from './realtime.ts';\nexport { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from './realtime.ts';\nexport {\n connectRealtime,\n listen,\n registerRealtimeApiProvider,\n registerSpeechApiProvider,\n registerTranscriptionApiProvider,\n speak,\n transcribe,\n} from './registry.ts';\nexport type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';\nexport { SpeechEventStream } from './speech.ts';\nexport type { VoiceAgentEvent, VoiceMessageChange } from './spoken.ts';\nexport type {\n BindThinkerOptions,\n BoundThinker,\n Thinker,\n ThinkerCaption,\n ThinkerPhase,\n ThinkerUpdate,\n} from './talker.ts';\nexport { askAgent, bindThinker, talkerTools } from './talker.ts';\nexport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEvent,\n TranscriptSegment,\n} from './transcription.ts';\nexport { TranscriptionEventStream } from './transcription.ts';\nexport type {\n BargeIn,\n CaptionEvent,\n EchoRecommendation,\n VoiceConversation,\n VoiceCut,\n VoiceModels,\n VoicePhase,\n VoiceSession,\n VoiceTurnMeta,\n VoiceTurnOptions,\n} from './turns.ts';\nexport { voiceTurns } from './turns.ts';\nexport type {\n AudioCost,\n AudioCounters,\n AudioUsage,\n RealtimeAudioCost,\n SpeechAudioCost,\n TranscriptionAudioCost,\n} from './usage.ts';\nexport { addAudioCounters, priceAudio } from './usage.ts';\nexport type { VadHook, VadUpdate } from './vad.ts';\nexport { energyVad } from './vad.ts';\n"],
|
|
5
|
+
"mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAiBlC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAYP,SAAS,cAAc,gBAAgB,gBAAgB,sBAAsB;AAC7E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,yBAAyB;AAUlC,SAAS,UAAU,aAAa,mBAAmB;AASnD,SAAS,gCAAgC;AAazC,SAAS,kBAAkB;AAS3B,SAAS,kBAAkB,kBAAkB;AAE7C,SAAS,iBAAiB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { registerVoiceModel } from "../models.js";
|
|
2
|
+
import { registerRealtimeApiProvider } from "../registry.js";
|
|
3
|
+
import { connectVendorSession, pcmFormat } from "./session.js";
|
|
4
|
+
const voices = [
|
|
5
|
+
{ id: "marin", name: "Marin" },
|
|
6
|
+
{ id: "cedar", name: "Cedar" },
|
|
7
|
+
{ id: "alloy", name: "Alloy" },
|
|
8
|
+
{ id: "ash", name: "Ash" },
|
|
9
|
+
{ id: "ballad", name: "Ballad" },
|
|
10
|
+
{ id: "coral", name: "Coral" },
|
|
11
|
+
{ id: "echo", name: "Echo" },
|
|
12
|
+
{ id: "sage", name: "Sage" },
|
|
13
|
+
{ id: "shimmer", name: "Shimmer" },
|
|
14
|
+
{ id: "verse", name: "Verse" }
|
|
15
|
+
];
|
|
16
|
+
const audioCost = { perInputMinute: 0.0192, perOutputMinute: 0.0384 };
|
|
17
|
+
function realtimeModel(id, name) {
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
name,
|
|
21
|
+
api: "openai-realtime",
|
|
22
|
+
provider: "openai",
|
|
23
|
+
baseUrl: "wss://api.openai.com/v1/realtime",
|
|
24
|
+
type: "realtime",
|
|
25
|
+
transports: ["websocket", "webrtc"],
|
|
26
|
+
toolCalls: "blocking",
|
|
27
|
+
inputFormat: pcmFormat(24e3),
|
|
28
|
+
outputFormat: pcmFormat(24e3),
|
|
29
|
+
resumable: false,
|
|
30
|
+
ephemeralAuth: true,
|
|
31
|
+
voices,
|
|
32
|
+
provisioning: "none",
|
|
33
|
+
delivery: { interrupt: "native", when_idle: "approximate", silent: "native" },
|
|
34
|
+
truncation: "client",
|
|
35
|
+
liveToolUpdate: true,
|
|
36
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
37
|
+
audioCost
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const gptRealtime2 = realtimeModel("gpt-realtime-2", "GPT Realtime 2");
|
|
41
|
+
const gptRealtime21 = realtimeModel("gpt-realtime-2.1", "GPT Realtime 2.1");
|
|
42
|
+
function registerOpenAIRealtime(options = {}) {
|
|
43
|
+
const unregisterModels = [gptRealtime2, gptRealtime21].map(registerVoiceModel);
|
|
44
|
+
const unregisterApi = registerRealtimeApiProvider({
|
|
45
|
+
api: "openai-realtime",
|
|
46
|
+
connect: (model, context, connectOptions) => connectVendorSession(model, context, connectOptions, { ...options, dialect: "openai" })
|
|
47
|
+
});
|
|
48
|
+
return () => {
|
|
49
|
+
for (const unregister of unregisterModels) unregister();
|
|
50
|
+
unregisterApi();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
gptRealtime2,
|
|
55
|
+
gptRealtime21,
|
|
56
|
+
registerOpenAIRealtime
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/openai-realtime/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { RealtimeModel } from '../models.ts';\nimport { registerVoiceModel } from '../models.ts';\nimport { registerRealtimeApiProvider } from '../registry.ts';\nimport { type AdapterOptions, connectVendorSession, pcmFormat } from './session.ts';\n\nconst voices = [\n { id: 'marin', name: 'Marin' },\n { id: 'cedar', name: 'Cedar' },\n { id: 'alloy', name: 'Alloy' },\n { id: 'ash', name: 'Ash' },\n { id: 'ballad', name: 'Ballad' },\n { id: 'coral', name: 'Coral' },\n { id: 'echo', name: 'Echo' },\n { id: 'sage', name: 'Sage' },\n { id: 'shimmer', name: 'Shimmer' },\n { id: 'verse', name: 'Verse' },\n];\n\nconst audioCost = { perInputMinute: 0.0192, perOutputMinute: 0.0384 };\n\nfunction realtimeModel(id: string, name: string): RealtimeModel<'openai-realtime'> {\n return {\n id,\n name,\n api: 'openai-realtime',\n provider: 'openai',\n baseUrl: 'wss://api.openai.com/v1/realtime',\n type: 'realtime',\n transports: ['websocket', 'webrtc'],\n toolCalls: 'blocking',\n inputFormat: pcmFormat(24000),\n outputFormat: pcmFormat(24000),\n resumable: false,\n ephemeralAuth: true,\n voices,\n provisioning: 'none',\n delivery: { interrupt: 'native', when_idle: 'approximate', silent: 'native' },\n truncation: 'client',\n liveToolUpdate: true,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n audioCost,\n };\n}\n\nexport const gptRealtime2 = realtimeModel('gpt-realtime-2', 'GPT Realtime 2');\n\nexport const gptRealtime21 = realtimeModel('gpt-realtime-2.1', 'GPT Realtime 2.1');\n\nexport function registerOpenAIRealtime(options: AdapterOptions = {}): () => void {\n const unregisterModels = [gptRealtime2, gptRealtime21].map(registerVoiceModel);\n const unregisterApi = registerRealtimeApiProvider({\n api: 'openai-realtime',\n connect: (model, context, connectOptions) =>\n connectVendorSession(model, context, connectOptions, { ...options, dialect: 'openai' }),\n });\n return () => {\n for (const unregister of unregisterModels) unregister();\n unregisterApi();\n };\n}\n\nexport type { AdapterOptions, VoiceWireFactory } from './session.ts';\nexport type { VoiceSocketConstructor, VoiceWire, VoiceWireTarget } from './socket.ts';\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAA0B;AACnC,SAAS,mCAAmC;AAC5C,SAA8B,sBAAsB,iBAAiB;AAErE,MAAM,SAAS;AAAA,EACb,EAAE,IAAI,SAAS,MAAM,QAAQ;AAAA,EAC7B,EAAE,IAAI,SAAS,MAAM,QAAQ;AAAA,EAC7B,EAAE,IAAI,SAAS,MAAM,QAAQ;AAAA,EAC7B,EAAE,IAAI,OAAO,MAAM,MAAM;AAAA,EACzB,EAAE,IAAI,UAAU,MAAM,SAAS;AAAA,EAC/B,EAAE,IAAI,SAAS,MAAM,QAAQ;AAAA,EAC7B,EAAE,IAAI,QAAQ,MAAM,OAAO;AAAA,EAC3B,EAAE,IAAI,QAAQ,MAAM,OAAO;AAAA,EAC3B,EAAE,IAAI,WAAW,MAAM,UAAU;AAAA,EACjC,EAAE,IAAI,SAAS,MAAM,QAAQ;AAC/B;AAEA,MAAM,YAAY,EAAE,gBAAgB,QAAQ,iBAAiB,OAAO;AAEpE,SAAS,cAAc,IAAY,MAAgD;AACjF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,KAAK;AAAA,IACL,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,YAAY,CAAC,aAAa,QAAQ;AAAA,IAClC,WAAW;AAAA,IACX,aAAa,UAAU,IAAK;AAAA,IAC5B,cAAc,UAAU,IAAK;AAAA,IAC7B,WAAW;AAAA,IACX,eAAe;AAAA,IACf;AAAA,IACA,cAAc;AAAA,IACd,UAAU,EAAE,WAAW,UAAU,WAAW,eAAe,QAAQ,SAAS;AAAA,IAC5E,YAAY;AAAA,IACZ,gBAAgB;AAAA,IAChB,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,EAAE;AAAA,IACzD;AAAA,EACF;AACF;AAEO,MAAM,eAAe,cAAc,kBAAkB,gBAAgB;AAErE,MAAM,gBAAgB,cAAc,oBAAoB,kBAAkB;AAE1E,SAAS,uBAAuB,UAA0B,CAAC,GAAe;AAC/E,QAAM,mBAAmB,CAAC,cAAc,aAAa,EAAE,IAAI,kBAAkB;AAC7E,QAAM,gBAAgB,4BAA4B;AAAA,IAChD,KAAK;AAAA,IACL,SAAS,CAAC,OAAO,SAAS,mBACxB,qBAAqB,OAAO,SAAS,gBAAgB,EAAE,GAAG,SAAS,SAAS,SAAS,CAAC;AAAA,EAC1F,CAAC;AACD,SAAO,MAAM;AACX,eAAW,cAAc,iBAAkB,YAAW;AACtD,kBAAc;AAAA,EAChB;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { blankUsage } from "../blank-usage.js";
|
|
2
|
+
import { decodeAudioBase64, encodeAudioBase64 } from "../convert.js";
|
|
3
|
+
import { VoiceRequestError } from "../errors.js";
|
|
4
|
+
import { planTruncation, RealtimeLedger, realtimeSocket } from "../realtime.js";
|
|
5
|
+
import {
|
|
6
|
+
openVoiceSocket
|
|
7
|
+
} from "./socket.js";
|
|
8
|
+
function record(value) {
|
|
9
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function text(value) {
|
|
13
|
+
return typeof value === "string" ? value : "";
|
|
14
|
+
}
|
|
15
|
+
function credential(options) {
|
|
16
|
+
const token = options?.token?.value;
|
|
17
|
+
if (typeof token === "string" && token.length > 0) return token;
|
|
18
|
+
const apiKey = options?.apiKey;
|
|
19
|
+
if (typeof apiKey === "string" && apiKey.length > 0) return apiKey;
|
|
20
|
+
throw new VoiceRequestError("error", "missing realtime credential");
|
|
21
|
+
}
|
|
22
|
+
function headersFor(options, secret, bearer) {
|
|
23
|
+
const headers = {};
|
|
24
|
+
if (bearer) headers.Authorization = `Bearer ${secret}`;
|
|
25
|
+
const extra = options?.headers;
|
|
26
|
+
if (extra === void 0) return headers;
|
|
27
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
28
|
+
if (value === null) delete headers[key];
|
|
29
|
+
else headers[key] = value;
|
|
30
|
+
}
|
|
31
|
+
return headers;
|
|
32
|
+
}
|
|
33
|
+
function sessionUrl(model, resumeHandle) {
|
|
34
|
+
const params = new URLSearchParams({ model: model.id });
|
|
35
|
+
if (resumeHandle !== void 0) params.set("conversation_id", resumeHandle);
|
|
36
|
+
return `${model.baseUrl}?${params.toString()}`;
|
|
37
|
+
}
|
|
38
|
+
function functionTool(tool) {
|
|
39
|
+
return {
|
|
40
|
+
type: "function",
|
|
41
|
+
name: tool.name,
|
|
42
|
+
description: tool.description,
|
|
43
|
+
parameters: tool.parameters
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function openaiSession(context, model) {
|
|
47
|
+
return {
|
|
48
|
+
type: "realtime",
|
|
49
|
+
instructions: context.instructions,
|
|
50
|
+
tools: context.tools.map(functionTool),
|
|
51
|
+
audio: {
|
|
52
|
+
input: {
|
|
53
|
+
format: { type: "audio/pcm", rate: model.inputFormat.sampleRate },
|
|
54
|
+
turn_detection: { type: "server_vad" }
|
|
55
|
+
},
|
|
56
|
+
output: {
|
|
57
|
+
format: { type: "audio/pcm", rate: model.outputFormat.sampleRate },
|
|
58
|
+
voice: context.voice
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function xaiSession(context, model, replace) {
|
|
64
|
+
const session = {
|
|
65
|
+
voice: context.voice,
|
|
66
|
+
instructions: context.instructions,
|
|
67
|
+
turn_detection: { type: "server_vad" },
|
|
68
|
+
tools: context.tools.map(functionTool),
|
|
69
|
+
resumption: { enabled: true },
|
|
70
|
+
audio: {
|
|
71
|
+
input: { format: { type: "audio/pcm", rate: model.inputFormat.sampleRate } },
|
|
72
|
+
output: { format: { type: "audio/pcm", rate: model.outputFormat.sampleRate } }
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
if (replace !== void 0) session.replace = replace;
|
|
76
|
+
return session;
|
|
77
|
+
}
|
|
78
|
+
function userItem(value) {
|
|
79
|
+
return {
|
|
80
|
+
type: "conversation.item.create",
|
|
81
|
+
item: {
|
|
82
|
+
type: "message",
|
|
83
|
+
role: "user",
|
|
84
|
+
content: [{ type: "input_text", text: value }]
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function responseCreate() {
|
|
89
|
+
return { type: "response.create" };
|
|
90
|
+
}
|
|
91
|
+
function resultText(result) {
|
|
92
|
+
const parts = [];
|
|
93
|
+
for (const part of result.content) {
|
|
94
|
+
if (part.type === "text") parts.push(part.text);
|
|
95
|
+
}
|
|
96
|
+
return parts.join("\n");
|
|
97
|
+
}
|
|
98
|
+
function toolOutput(callId, result) {
|
|
99
|
+
return {
|
|
100
|
+
type: "conversation.item.create",
|
|
101
|
+
item: { type: "function_call_output", call_id: callId, output: resultText(result) }
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function forceMessage(value) {
|
|
105
|
+
return {
|
|
106
|
+
type: "conversation.item.create",
|
|
107
|
+
item: {
|
|
108
|
+
type: "force_message",
|
|
109
|
+
role: "assistant",
|
|
110
|
+
content: [{ type: "output_text", text: value }]
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function spanFor(itemId, start, end) {
|
|
115
|
+
return { itemId, start, end };
|
|
116
|
+
}
|
|
117
|
+
function takeAudio(pending, itemId, format, end) {
|
|
118
|
+
const chunks = pending.chunks;
|
|
119
|
+
pending.chunks = [];
|
|
120
|
+
if (chunks.length === 0) return [];
|
|
121
|
+
const start = pending.cursor;
|
|
122
|
+
if (end !== void 0) pending.cursor = end;
|
|
123
|
+
const width = end === void 0 ? 0 : end - start;
|
|
124
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.data.length, 0);
|
|
125
|
+
let used = 0;
|
|
126
|
+
let cursor = start;
|
|
127
|
+
return chunks.map((chunk, index) => {
|
|
128
|
+
const event = {
|
|
129
|
+
type: "output_audio",
|
|
130
|
+
itemId,
|
|
131
|
+
chunk: { data: chunk.data, format, timestamp: chunk.timestamp }
|
|
132
|
+
};
|
|
133
|
+
if (end === void 0 || total === 0) return event;
|
|
134
|
+
const last = index === chunks.length - 1;
|
|
135
|
+
const next = last ? end : start + Math.round((used + chunk.data.length) / total * width);
|
|
136
|
+
used += chunk.data.length;
|
|
137
|
+
event.span = spanFor(itemId, cursor, next);
|
|
138
|
+
cursor = next;
|
|
139
|
+
return event;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function argumentsOf(value) {
|
|
143
|
+
if (typeof value !== "string") return {};
|
|
144
|
+
try {
|
|
145
|
+
const parsed = JSON.parse(value);
|
|
146
|
+
const object = record(parsed);
|
|
147
|
+
if (object === void 0) return {};
|
|
148
|
+
return object;
|
|
149
|
+
} catch {
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function responseUsage(value) {
|
|
154
|
+
const usage = record(value);
|
|
155
|
+
if (usage === void 0) return blankUsage();
|
|
156
|
+
const input = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
|
|
157
|
+
const output = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
|
|
158
|
+
return {
|
|
159
|
+
input,
|
|
160
|
+
output,
|
|
161
|
+
cacheRead: 0,
|
|
162
|
+
cacheWrite: 0,
|
|
163
|
+
totalTokens: input + output,
|
|
164
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
class VendorSession {
|
|
168
|
+
model;
|
|
169
|
+
#dialect;
|
|
170
|
+
#context;
|
|
171
|
+
#replace;
|
|
172
|
+
#wire;
|
|
173
|
+
#ledger = new RealtimeLedger();
|
|
174
|
+
#format;
|
|
175
|
+
#pending = /* @__PURE__ */ new Map();
|
|
176
|
+
#inputText = /* @__PURE__ */ new Map();
|
|
177
|
+
#queue = [];
|
|
178
|
+
#waiters = [];
|
|
179
|
+
#closed = false;
|
|
180
|
+
#usage;
|
|
181
|
+
#lastItem = "";
|
|
182
|
+
constructor(model, context, dialect, replace, wire) {
|
|
183
|
+
this.model = model;
|
|
184
|
+
this.#context = context;
|
|
185
|
+
this.#dialect = dialect;
|
|
186
|
+
this.#replace = replace;
|
|
187
|
+
this.#wire = wire;
|
|
188
|
+
this.#format = model.outputFormat;
|
|
189
|
+
}
|
|
190
|
+
start(options) {
|
|
191
|
+
this.#send(this.#sessionUpdate(this.#context));
|
|
192
|
+
const signal = options?.signal;
|
|
193
|
+
if (signal !== void 0) signal.addEventListener("abort", () => void this.close());
|
|
194
|
+
void this.#read();
|
|
195
|
+
}
|
|
196
|
+
appendAudio(chunk) {
|
|
197
|
+
this.#assertOpen();
|
|
198
|
+
this.#ledger.addInput(chunk);
|
|
199
|
+
this.#send({ type: "input_audio_buffer.append", audio: encodeAudioBase64(chunk.data) });
|
|
200
|
+
}
|
|
201
|
+
sendText(textValue) {
|
|
202
|
+
this.#assertOpen();
|
|
203
|
+
this.#ledger.addText();
|
|
204
|
+
this.#send(userItem(textValue));
|
|
205
|
+
this.#send(responseCreate());
|
|
206
|
+
}
|
|
207
|
+
inject(textValue, delivery) {
|
|
208
|
+
this.#assertOpen();
|
|
209
|
+
this.#ledger.submit(this.model.delivery, delivery, () => {
|
|
210
|
+
this.#assertOpen();
|
|
211
|
+
this.#ledger.addText();
|
|
212
|
+
this.#sendInject(textValue, delivery);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
respond(callId, result, delivery) {
|
|
216
|
+
this.#assertOpen();
|
|
217
|
+
const send = () => {
|
|
218
|
+
this.#send(toolOutput(callId, result));
|
|
219
|
+
if (delivery !== "silent") this.#send(responseCreate());
|
|
220
|
+
};
|
|
221
|
+
if (delivery === void 0) {
|
|
222
|
+
send();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
this.#ledger.submit(this.model.delivery, delivery, () => {
|
|
226
|
+
this.#assertOpen();
|
|
227
|
+
send();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
interrupt(heard) {
|
|
231
|
+
this.#assertOpen();
|
|
232
|
+
this.#send({ type: "response.cancel" });
|
|
233
|
+
const plan = planTruncation(this.model.truncation, heard);
|
|
234
|
+
if (plan.item !== void 0) {
|
|
235
|
+
this.#send({
|
|
236
|
+
type: "conversation.item.truncate",
|
|
237
|
+
item_id: plan.item.itemId,
|
|
238
|
+
content_index: 0,
|
|
239
|
+
audio_end_ms: plan.item.audioEndMs
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
this.#ledger.settle();
|
|
243
|
+
}
|
|
244
|
+
update(patch) {
|
|
245
|
+
this.#assertOpen();
|
|
246
|
+
if (patch.tools !== void 0 && !this.model.liveToolUpdate) {
|
|
247
|
+
throw new VoiceRequestError("error", "live tool update is unsupported");
|
|
248
|
+
}
|
|
249
|
+
let instructions = this.#context.instructions;
|
|
250
|
+
if (patch.instructions !== void 0) instructions = patch.instructions;
|
|
251
|
+
let tools = this.#context.tools;
|
|
252
|
+
if (patch.tools !== void 0) tools = patch.tools;
|
|
253
|
+
let voice = this.#context.voice;
|
|
254
|
+
if (patch.voice !== void 0) voice = patch.voice;
|
|
255
|
+
const context = { instructions, tools, voice };
|
|
256
|
+
if (patch.instructions !== void 0) this.#context.instructions = context.instructions;
|
|
257
|
+
if (patch.tools !== void 0) this.#context.tools = context.tools;
|
|
258
|
+
if (patch.voice !== void 0) this.#context.voice = context.voice;
|
|
259
|
+
this.#send(this.#sessionUpdate(context));
|
|
260
|
+
}
|
|
261
|
+
async close() {
|
|
262
|
+
if (this.#usage !== void 0) return { usage: this.#usage };
|
|
263
|
+
this.#ledger.settle();
|
|
264
|
+
this.#closed = true;
|
|
265
|
+
this.#wire.close();
|
|
266
|
+
this.#end();
|
|
267
|
+
this.#usage = this.#ledger.usage(this.model.audioCost);
|
|
268
|
+
return { usage: this.#usage };
|
|
269
|
+
}
|
|
270
|
+
async *[Symbol.asyncIterator]() {
|
|
271
|
+
while (true) {
|
|
272
|
+
const queued = this.#queue.shift();
|
|
273
|
+
if (queued !== void 0) {
|
|
274
|
+
yield queued;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (this.#closed) return;
|
|
278
|
+
const next = await new Promise((resolve) => {
|
|
279
|
+
this.#waiters.push(resolve);
|
|
280
|
+
});
|
|
281
|
+
if (next.done === true || next.value === void 0) return;
|
|
282
|
+
yield next.value;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
#sessionUpdate(context) {
|
|
286
|
+
if (this.#dialect === "openai") {
|
|
287
|
+
return { type: "session.update", session: openaiSession(context, this.model) };
|
|
288
|
+
}
|
|
289
|
+
return { type: "session.update", session: xaiSession(context, this.model, this.#replace) };
|
|
290
|
+
}
|
|
291
|
+
#sendInject(textValue, delivery) {
|
|
292
|
+
if (this.#dialect === "xai" && delivery !== "silent") {
|
|
293
|
+
this.#send(forceMessage(textValue));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
this.#send(userItem(textValue));
|
|
297
|
+
if (delivery !== "silent") this.#send(responseCreate());
|
|
298
|
+
}
|
|
299
|
+
#send(event) {
|
|
300
|
+
this.#wire.send(JSON.stringify(event));
|
|
301
|
+
}
|
|
302
|
+
#assertOpen() {
|
|
303
|
+
if (this.#closed) throw new VoiceRequestError("error", "session is closed");
|
|
304
|
+
}
|
|
305
|
+
#push(event) {
|
|
306
|
+
if (this.#closed) return;
|
|
307
|
+
this.#ledger.observe(event);
|
|
308
|
+
const waiter = this.#waiters.shift();
|
|
309
|
+
if (waiter !== void 0) waiter({ value: event, done: false });
|
|
310
|
+
else this.#queue.push(event);
|
|
311
|
+
}
|
|
312
|
+
#end() {
|
|
313
|
+
for (const waiter of this.#waiters) waiter({ value: void 0, done: true });
|
|
314
|
+
this.#waiters.length = 0;
|
|
315
|
+
}
|
|
316
|
+
#pendingFor(itemId) {
|
|
317
|
+
const existing = this.#pending.get(itemId);
|
|
318
|
+
if (existing !== void 0) return existing;
|
|
319
|
+
const created = { chunks: [], cursor: 0 };
|
|
320
|
+
this.#pending.set(itemId, created);
|
|
321
|
+
return created;
|
|
322
|
+
}
|
|
323
|
+
#audio(itemId, encoded) {
|
|
324
|
+
this.#lastItem = itemId;
|
|
325
|
+
const pending = this.#pendingFor(itemId);
|
|
326
|
+
pending.chunks.push({
|
|
327
|
+
data: decodeAudioBase64(encoded),
|
|
328
|
+
format: this.#format,
|
|
329
|
+
timestamp: 0
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
#transcript(itemId, delta, final) {
|
|
333
|
+
const pending = this.#pendingFor(itemId);
|
|
334
|
+
const end = pending.cursor + delta.length;
|
|
335
|
+
const tagged = delta.length === 0 ? void 0 : end;
|
|
336
|
+
for (const event of takeAudio(pending, itemId, this.#format, tagged)) this.#push(event);
|
|
337
|
+
this.#push({ type: "output_transcript", itemId, text: delta, final });
|
|
338
|
+
}
|
|
339
|
+
#input(itemId, transcript, mode, final) {
|
|
340
|
+
const previous = this.#inputText.get(itemId) ?? "";
|
|
341
|
+
const next = mode === "append" ? `${previous}${transcript}` : transcript;
|
|
342
|
+
this.#inputText.set(itemId, next);
|
|
343
|
+
this.#push({ type: "input_transcript", itemId, text: next, final });
|
|
344
|
+
}
|
|
345
|
+
#field(value, key) {
|
|
346
|
+
const object = record(value);
|
|
347
|
+
if (object === void 0) return "";
|
|
348
|
+
return text(object[key]);
|
|
349
|
+
}
|
|
350
|
+
#functionCall(event) {
|
|
351
|
+
this.#push({
|
|
352
|
+
type: "tool_call",
|
|
353
|
+
blocking: this.model.toolCalls === "blocking",
|
|
354
|
+
call: {
|
|
355
|
+
type: "toolCall",
|
|
356
|
+
id: text(event.call_id),
|
|
357
|
+
name: text(event.name),
|
|
358
|
+
arguments: argumentsOf(event.arguments)
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
#responseDone(event) {
|
|
363
|
+
const response = record(event.response);
|
|
364
|
+
const output = response === void 0 ? void 0 : response.output;
|
|
365
|
+
const first = Array.isArray(output) ? record(output[0]) : void 0;
|
|
366
|
+
let itemId = this.#lastItem;
|
|
367
|
+
if (first !== void 0 && text(first.id).length > 0) itemId = text(first.id);
|
|
368
|
+
const pending = this.#pendingFor(itemId);
|
|
369
|
+
for (const audioEvent of takeAudio(pending, itemId, this.#format, void 0)) {
|
|
370
|
+
this.#push(audioEvent);
|
|
371
|
+
}
|
|
372
|
+
const status = response === void 0 ? "" : text(response.status);
|
|
373
|
+
const usage = response === void 0 ? void 0 : response.usage;
|
|
374
|
+
this.#push({
|
|
375
|
+
type: "response_done",
|
|
376
|
+
itemId,
|
|
377
|
+
interrupted: status === "cancelled",
|
|
378
|
+
usage: responseUsage(usage)
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
#serverError(event) {
|
|
382
|
+
const detail = record(event.error);
|
|
383
|
+
const code = detail === void 0 ? "" : text(detail.code);
|
|
384
|
+
const message = detail === void 0 ? "" : text(detail.message);
|
|
385
|
+
let fatal = false;
|
|
386
|
+
if (code === "max_duration") fatal = true;
|
|
387
|
+
if (code === "session_expired") fatal = true;
|
|
388
|
+
this.#push({ type: "error", error: message, fatal });
|
|
389
|
+
}
|
|
390
|
+
#apply(event) {
|
|
391
|
+
const type = text(event.type);
|
|
392
|
+
if (type === "session.created") {
|
|
393
|
+
this.#push({ type: "session_start", sessionId: this.#field(event.session, "id") });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (type === "conversation.created") {
|
|
397
|
+
this.#push({ type: "resumption", handle: this.#field(event.conversation, "id") });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (type === "input_audio_buffer.speech_started") {
|
|
401
|
+
this.#push({ type: "input_speech_start" });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (type === "conversation.item.input_audio_transcription.delta") {
|
|
405
|
+
this.#input(text(event.item_id), text(event.delta), "append", false);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (type === "conversation.item.input_audio_transcription.updated") {
|
|
409
|
+
this.#input(text(event.item_id), text(event.transcript), "replace", false);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (type === "conversation.item.input_audio_transcription.completed") {
|
|
413
|
+
this.#input(text(event.item_id), text(event.transcript), "replace", true);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (type === "response.output_audio.delta") {
|
|
417
|
+
this.#audio(text(event.item_id), text(event.delta));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (type === "response.output_audio_transcript.delta") {
|
|
421
|
+
this.#transcript(text(event.item_id), text(event.delta), false);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (type === "response.output_audio_transcript.done") {
|
|
425
|
+
this.#transcript(text(event.item_id), text(event.transcript), true);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (type === "response.function_call_arguments.done") {
|
|
429
|
+
this.#functionCall(event);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (type === "response.done") {
|
|
433
|
+
this.#responseDone(event);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (type === "error") this.#serverError(event);
|
|
437
|
+
}
|
|
438
|
+
async #read() {
|
|
439
|
+
try {
|
|
440
|
+
for await (const raw of this.#wire.incoming) {
|
|
441
|
+
let parsed;
|
|
442
|
+
try {
|
|
443
|
+
parsed = JSON.parse(raw);
|
|
444
|
+
} catch {
|
|
445
|
+
this.#push({ type: "error", error: "invalid realtime event", fatal: false });
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const event = record(parsed);
|
|
449
|
+
if (event === void 0 || typeof event.type !== "string") continue;
|
|
450
|
+
this.#apply(event);
|
|
451
|
+
}
|
|
452
|
+
} catch (error) {
|
|
453
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
454
|
+
this.#push({ type: "error", error: message, fatal: true });
|
|
455
|
+
} finally {
|
|
456
|
+
this.#closed = true;
|
|
457
|
+
this.#end();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function connectVendorSession(model, context, options, vendor) {
|
|
462
|
+
if (options?.signal?.aborted === true) {
|
|
463
|
+
throw new VoiceRequestError("aborted", "session aborted");
|
|
464
|
+
}
|
|
465
|
+
if (options?.transport === "webrtc" && vendor.wire === void 0) {
|
|
466
|
+
throw new VoiceRequestError("error", "webrtc requires a data channel wire");
|
|
467
|
+
}
|
|
468
|
+
const secret = credential(options);
|
|
469
|
+
const token = options?.token?.value;
|
|
470
|
+
const ephemeral = typeof token === "string" && token.length > 0;
|
|
471
|
+
const protocols = [];
|
|
472
|
+
if (vendor.dialect === "openai") protocols.push(`openai-insecure-api-key.${secret}`, "realtime");
|
|
473
|
+
else if (ephemeral) protocols.push(`xai-client-secret.${secret}`);
|
|
474
|
+
else protocols.push("realtime");
|
|
475
|
+
const resumeHandle = vendor.dialect === "xai" ? options?.resumeHandle : void 0;
|
|
476
|
+
const socket = realtimeSocket(sessionUrl(model, resumeHandle), options, protocols);
|
|
477
|
+
const target = {
|
|
478
|
+
url: socket.url,
|
|
479
|
+
protocols: socket.protocols,
|
|
480
|
+
headers: headersFor(options, secret, vendor.dialect === "openai" || !ephemeral)
|
|
481
|
+
};
|
|
482
|
+
const open = vendor.wire !== void 0 ? vendor.wire : (wireTarget) => openVoiceSocket(wireTarget, vendor.socket);
|
|
483
|
+
const session = new VendorSession(model, context, vendor.dialect, vendor.replace, open(target));
|
|
484
|
+
session.start(options);
|
|
485
|
+
return session;
|
|
486
|
+
}
|
|
487
|
+
function pcmFormat(sampleRate) {
|
|
488
|
+
return { encoding: "pcm16", sampleRate, channels: 1 };
|
|
489
|
+
}
|
|
490
|
+
export {
|
|
491
|
+
connectVendorSession,
|
|
492
|
+
pcmFormat
|
|
493
|
+
};
|
|
494
|
+
//# sourceMappingURL=session.js.map
|