@pyai/sdk 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -31,6 +31,20 @@ export interface Voice {
31
31
  name?: string;
32
32
  gender?: string;
33
33
  region?: string;
34
+ language?: string;
35
+ /** Customer-facing quality tier. */
36
+ tier?: "standard" | "natural";
37
+ /** Permanent convenience inputs accepted on the advertised surfaces. */
38
+ aliases?: string[];
39
+ /** Product surfaces on which this stock voice can be selected. */
40
+ available_on?: Array<"speak" | "omni">;
41
+ /** Accepted Speak delivery modes. Empty means the voice has no Speak surface. */
42
+ synthesis_modes?: Array<"streaming" | "async">;
43
+ /** Voice-specific amount on top of the selected product's base rate. */
44
+ pricing?: {
45
+ included_in_base_price: boolean;
46
+ additional_price_usd_per_minute: number;
47
+ };
34
48
  [k: string]: unknown;
35
49
  }
36
50
  export interface ListResponse<T> {
@@ -82,6 +96,13 @@ export interface SpeechParams {
82
96
  input: string;
83
97
  voice?: string;
84
98
  model?: SpeakModel;
99
+ /**
100
+ * Delivery mode. The API defaults to true and every catalog voice accepts
101
+ * both values. Set false when you want one complete buffered body with a
102
+ * `Content-Length`. A voice whose serving fleet has no streaming lane is
103
+ * rendered buffered either way and says so with `x-pyai-stream: buffered`.
104
+ */
105
+ stream?: boolean;
85
106
  /**
86
107
  * Output container/codec, resampled+encoded server-side. One of
87
108
  * {@link SpeechFormat}, anything else is a `400 unsupported_format`. Omit for
@@ -119,8 +140,57 @@ export interface CreateJobParams {
119
140
  diarize?: boolean;
120
141
  channel?: boolean;
121
142
  numerals?: boolean;
143
+ smart_format?: boolean;
144
+ dictation?: boolean;
145
+ drop_fillers?: boolean;
146
+ /**
147
+ * Per-job names, brands, products, or distinctive terms. PyAI keeps up to
148
+ * five valid entries after trimming, deduplication, and common-word
149
+ * filtering.
150
+ */
151
+ vocabulary?: string[];
122
152
  output_formats?: Array<"json" | "srt" | "vtt">;
123
153
  webhook_url?: string;
154
+ call_id?: string;
155
+ pack_id?: string;
156
+ call_direction?: "inbound" | "outbound";
157
+ customer_name?: string;
158
+ }
159
+ export type HearVocabularyProfile = "batch" | "hear_stream";
160
+ export interface HearVocabularyInput {
161
+ /** Organization-owned names, brands, products, or distinctive terms. */
162
+ terms: string[];
163
+ /** Stored terms remain inert unless a use case is selected here. */
164
+ enabledFor: HearVocabularyProfile[];
165
+ }
166
+ export interface HearVocabulary {
167
+ object: "hear.vocabulary";
168
+ /** Sanitized stored list, at most five terms. */
169
+ terms: string[];
170
+ /** Use cases that may add stored suggestions. */
171
+ enabled_for: HearVocabularyProfile[];
172
+ /** Unix milliseconds, or null before the first save. */
173
+ updated_at: number | null;
174
+ }
175
+ export interface AgentConfig {
176
+ name?: string;
177
+ persona_system_prompt?: string | null;
178
+ greeting?: string | null;
179
+ voice_id?: string | null;
180
+ language?: "en" | "fr" | "es" | "de" | "hi" | null;
181
+ /**
182
+ * Opt-in speech-recognition vocabulary. PyAI sanitizes and keeps at most
183
+ * five terms. An empty list or null turns it off.
184
+ */
185
+ vocabulary?: string[] | null;
186
+ [key: string]: unknown;
187
+ }
188
+ export interface Agent {
189
+ object: "agent";
190
+ agent_id: string;
191
+ name: string;
192
+ vocabulary: string[];
193
+ [key: string]: unknown;
124
194
  }
125
195
  export interface RealtimeOptions {
126
196
  /**
@@ -183,10 +253,16 @@ export declare const HearFrameType: {
183
253
  readonly Error: "error";
184
254
  };
185
255
  export type HearFrameType = (typeof HearFrameType)[keyof typeof HearFrameType];
186
- /** WebSocket close codes used across the PyAI realtime/streaming surfaces. */
256
+ /**
257
+ * WebSocket close codes used across the PyAI realtime/streaming surfaces.
258
+ * Browser callers may initiate only Normal or values in 3000-4999; the
259
+ * standards-reserved values below are server close observations.
260
+ */
187
261
  export declare const WSCloseCode: {
188
262
  /** Normal closure. */
189
263
  readonly Normal: 1000;
264
+ /** Browser-safe private application code for a malformed peer protocol frame. */
265
+ readonly ProtocolViolation: 4002;
190
266
  /** Auth/policy: bad key, missing scope, or revoked token. */
191
267
  readonly PolicyViolation: 1008;
192
268
  /** Engine/internal error. */
@@ -214,6 +290,7 @@ export declare const ErrorCode: {
214
290
  readonly IdempotencyConflict: "idempotency_conflict";
215
291
  readonly NotFound: "not_found";
216
292
  readonly NumberInUse: "number_in_use";
293
+ readonly UnsupportedToolTransport: "unsupported_tool_transport";
217
294
  };
218
295
  export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
219
296
  /** A top-3 knowledge-base passage attached to Cue (grounded) finals. */
@@ -303,28 +380,59 @@ export interface WebSocketLike {
303
380
  reason: string;
304
381
  }) => void) | null;
305
382
  }
383
+ /**
384
+ * The non-secret marker every PyAI realtime client offers ALONGSIDE its
385
+ * `pyai-key.<KEY>` credential token.
386
+ *
387
+ * A browser cannot set `Authorization` on a WebSocket, so the key rides in
388
+ * `Sec-WebSocket-Protocol`. RFC 6455 makes the server echo the subprotocol it
389
+ * SELECTS, so a server that selects the credential publishes the credential —
390
+ * which api.pyai.com did until 2026-09-07. The edge now echoes only this
391
+ * marker. Offering it is not optional for Node clients: `ws` throws
392
+ * "Server sent no subprotocol" when it offered subprotocols and the 101
393
+ * selected none, and RFC 6455 lets the server select only a value the client
394
+ * actually offered.
395
+ */
396
+ export declare const REALTIME_SUBPROTOCOL_MARKER = "pyai.v1";
306
397
  export type WebSocketCtor = new (url: string, protocols?: string | string[]) => WebSocketLike;
307
398
  export interface HearStreamOptions {
308
399
  /** Streaming STT model. Server default `pyai-hear`. */
309
400
  model?: string;
310
- /**
311
- * Hear is English-only. Set `"en"` explicitly; omission also means English
312
- * and does not enable language detection. Other values receive
313
- * `400 unsupported_language`.
314
- */
315
- language?: "en";
401
+ /** Omit or use auto for automatic detection; an explicit code pins recognition. */
402
+ language?: "auto" | "en" | "es" | "fr" | "de" | "hi" | "it" | "pt" | "nl";
316
403
  /** Input PCM sample rate in Hz. Default 16000 server-side. */
317
404
  sampleRate?: number;
318
405
  /** Audio frame encoding. Default "pcm16". */
319
- encoding?: "pcm16" | "opus";
406
+ encoding?: "pcm16";
320
407
  /** Emit eager partial hypotheses. Default true server-side. */
321
408
  interimResults?: boolean;
322
409
  /**
323
- * Format spoken numbers as digits in the transcript (e.g. "one two three" →
324
- * "123"). Useful for voice agents that read back phone numbers, codes, and
325
- * amounts. Default false (spoken form). Forwards `?numerals=true` on the URL.
410
+ * Tri-state number formatting on **final** transcripts. `true` forces digits,
411
+ * `false` keeps spoken form, omitted keeps the live engine default (ITN on).
412
+ * Never applied to interim partials. Independent of `smartFormat`.
326
413
  */
327
414
  numerals?: boolean;
415
+ /**
416
+ * Opt-in English punctuation and sentence capitalization on **final**
417
+ * transcripts only. Interim partials are never formatted. Default false.
418
+ */
419
+ smartFormat?: boolean;
420
+ /**
421
+ * Spoken punctuation commands (`period`, `comma`, `new paragraph`,
422
+ * `question mark`) on finals only. Separate from `smartFormat`. Off by default.
423
+ */
424
+ dictation?: boolean;
425
+ /**
426
+ * Strip `um` / `uh` / `umm` / `uhh` / `er` on finals. Off by default.
427
+ * Do not enable on legal or compliance audio by default.
428
+ */
429
+ dropFillers?: boolean;
430
+ /**
431
+ * Per-session names, brands, products, or other distinctive terms. PyAI
432
+ * sanitizes up to five effective terms. Request terms come first, then stored
433
+ * `hear_stream` suggestions fill remaining slots. The list is fixed at open.
434
+ */
435
+ vocabulary?: string[];
328
436
  /**
329
437
  * Minimum trailing-pause length before an utterance may end (50-5000 ms).
330
438
  * Turn detection may wait longer, bounded at `max(endpointingMs, 1500)`.
@@ -439,6 +547,8 @@ export declare const OmniEvent: {
439
547
  readonly Flush: "flush";
440
548
  /** Engine requests a client-loop tool invocation. */
441
549
  readonly ToolCall: "tool_call";
550
+ /** A write tool is waiting for the caller to confirm; it has not run. */
551
+ readonly ToolConfirmationRequired: "tool_confirmation_required";
442
552
  /** Session is closing; see close code. */
443
553
  readonly SessionEnd: "session_end";
444
554
  /** Server fault frame. */
@@ -471,8 +581,6 @@ export interface OmniToolDef {
471
581
  name: string;
472
582
  description?: string;
473
583
  parameters?: Record<string, unknown>;
474
- /** When set, engine-POST mode. Omit for client-loop (default). */
475
- endpoint?: string;
476
584
  }
477
585
  export interface OmniToolCallFrame {
478
586
  event: "tool_call";
@@ -508,7 +616,9 @@ export interface OmniConfigure {
508
616
  * staged, see the Language support reference.
509
617
  */
510
618
  language?: "en" | "fr" | "es" | "de" | "hi";
511
- /** Function calling definitions (client-loop when `endpoint` is omitted). */
619
+ /** Function calling: hosted catalog names, client-loop schemas, or names of
620
+ * server tools already registered with POST /v1/tools. An inline `endpoint`
621
+ * is rejected with `unsupported_tool_transport`. */
512
622
  tools?: OmniToolDef[];
513
623
  /** Forward-compatible: any other key the engine honors. */
514
624
  [k: string]: unknown;
@@ -874,8 +984,45 @@ export interface RecapCallSummary {
874
984
  created_at?: number;
875
985
  completed_at?: number | null;
876
986
  }
987
+ export interface RecapActionItem {
988
+ owner?: string | null;
989
+ task: string;
990
+ due?: string | null;
991
+ }
992
+ export interface RecapTalkRatio {
993
+ agent: number;
994
+ customer: number;
995
+ }
996
+ export interface RecapSignal {
997
+ kind: string;
998
+ text: string;
999
+ at_s?: number | null;
1000
+ }
1001
+ export interface RecapRecord {
1002
+ format: "recap.record.v1";
1003
+ tldr: string | null;
1004
+ summary: string | null;
1005
+ action_items: RecapActionItem[];
1006
+ disposition: string | null;
1007
+ next_steps: string | null;
1008
+ talk_ratio: RecapTalkRatio | null;
1009
+ signals: RecapSignal[];
1010
+ fields: Record<string, unknown>;
1011
+ rep?: Record<string, unknown>;
1012
+ manager?: Record<string, unknown>;
1013
+ ops?: Record<string, unknown>;
1014
+ }
877
1015
  export interface RecapCall extends RecapCallSummary {
878
- record?: unknown;
1016
+ record?: RecapRecord;
1017
+ transcript?: {
1018
+ format: "utterances.v1";
1019
+ utterances: Array<{
1020
+ speaker_role: "agent" | "customer";
1021
+ text: string;
1022
+ offset_s: number;
1023
+ duration_s: number;
1024
+ }>;
1025
+ };
879
1026
  error?: string | null;
880
1027
  crm_write_status?: string | null;
881
1028
  }
@@ -994,19 +1141,43 @@ export declare class PyAI {
994
1141
  list: (params?: {
995
1142
  gender?: string;
996
1143
  region?: string;
1144
+ language?: string;
1145
+ tier?: "standard" | "natural";
1146
+ q?: string;
997
1147
  }) => Promise<ListResponse<Voice>>;
998
1148
  get: (id: string) => Promise<Voice>;
999
1149
  };
1150
+ hear: {
1151
+ vocabulary: {
1152
+ /** Read organization-owned vocabulary. Scope `hear:configure`. */
1153
+ get: () => Promise<HearVocabulary>;
1154
+ /** Replace vocabulary and activation profiles. Scope `hear:configure`. */
1155
+ set: (input: HearVocabularyInput) => Promise<HearVocabulary>;
1156
+ };
1157
+ };
1158
+ agents: {
1159
+ list: () => Promise<ListResponse<Agent>>;
1160
+ get: (agentId: string) => Promise<Agent>;
1161
+ create: (input: AgentConfig & {
1162
+ name: string;
1163
+ }) => Promise<Agent>;
1164
+ update: (agentId: string, patch: AgentConfig) => Promise<Agent>;
1165
+ delete: (agentId: string) => Promise<{
1166
+ object: "agent.deleted";
1167
+ agent_id: string;
1168
+ deleted: boolean;
1169
+ }>;
1170
+ };
1000
1171
  audio: {
1001
1172
  /** Text-to-speech. Returns the raw audio bytes (default WAV). */
1002
1173
  speech: (params: SpeechParams) => Promise<ArrayBuffer>;
1003
1174
  /**
1004
1175
  * Text-to-speech, streamed. Resolves as soon as the response headers arrive
1005
1176
  * with the body as a `ReadableStream` of audio bytes, so you can start
1006
- * playback or forward the audio at the first chunk (the engine's
1007
- * time-to-first-byte is tens of ms) instead of buffering the whole clip.
1008
- * Use `mp3` for the smoothest progressive playback. Returns an async
1009
- * iterable of Uint8Array chunks.
1177
+ * playback or forward the audio at the first chunk. Use `pcm`, `wav`,
1178
+ * or G.711 for streaming; `mp3` and `opus` are buffered server-side.
1179
+ * Consume the stream immediately and cancel its reader when stopping early.
1180
+ * HTTP connection reuse and protocol negotiation are controlled by fetch.
1010
1181
  */
1011
1182
  speechStream: (params: SpeechParams) => Promise<ReadableStream<Uint8Array>>;
1012
1183
  /** Synchronous speech-to-text (multipart upload). */
@@ -1015,11 +1186,13 @@ export declare class PyAI {
1015
1186
  file: Blob;
1016
1187
  filename?: string;
1017
1188
  model?: string;
1018
- /**
1019
- * Hear is English-only. Omission means English, not auto-detect; other
1020
- * values receive `400 unsupported_language`.
1021
- */
1022
- language?: "en";
1189
+ /** Optional language hint. Omission enables automatic detection. */
1190
+ language?: "en" | "es" | "fr" | "de" | "hi" | "it" | "pt" | "nl";
1191
+ numerals?: boolean;
1192
+ smart_format?: boolean;
1193
+ dictation?: boolean;
1194
+ drop_fillers?: boolean;
1195
+ vocabulary?: string[];
1023
1196
  response_format?: "json" | "text" | "verbose_json";
1024
1197
  /**
1025
1198
  * Deterministic seed for reproducible eval runs. Forward-compatible:
@@ -1200,7 +1373,7 @@ export declare class PyAI {
1200
1373
  * realtime. **Call this from your server** with a secret key holding
1201
1374
  * `omni:session`; never ship the secret key to a page. Hand the returned
1202
1375
  * `token` to the browser, which connects with
1203
- * `new WebSocket(session.url, ["pyai-key." + session.token])`. The token
1376
+ * `new WebSocket(session.url, ["pyai.v1", "pyai-key." + session.token])`. The token
1204
1377
  * expires after `ttlSeconds` (default 60s) and only works from
1205
1378
  * `allowedOrigins`. Scope `omni:session`.
1206
1379
  */
@@ -1218,8 +1391,15 @@ export declare class PyAI {
1218
1391
  };
1219
1392
  /** Build the canonical Omni WebSocket URL. */
1220
1393
  realtimeURL(opts?: RealtimeOptions): string;
1221
- /** The subprotocol that carries the key on a WS upgrade (browser-safe auth). */
1394
+ /** The subprotocol token that carries the key on a WS upgrade. */
1222
1395
  realtimeSubprotocol(): string;
1396
+ /**
1397
+ * The full subprotocol list to open a PyAI WebSocket with:
1398
+ * `[marker, credential]`. Always pass BOTH — see
1399
+ * {@link REALTIME_SUBPROTOCOL_MARKER}. Pass `token` to use an ephemeral
1400
+ * session token (from `omni.createSession`) instead of the secret key.
1401
+ */
1402
+ realtimeSubprotocols(token?: string): string[];
1223
1403
  /** Build the Hear streaming-STT WebSocket URL (`/v1/audio/transcriptions/stream`). */
1224
1404
  hearStreamURL(opts?: HearStreamOptions): string;
1225
1405
  /** Build the AMD detection WebSocket URL (`/v1/amd/stream`). */
package/dist/index.js CHANGED
@@ -62,10 +62,16 @@ export const HearFrameType = {
62
62
  /** Server-side fault frame. */
63
63
  Error: "error",
64
64
  };
65
- /** WebSocket close codes used across the PyAI realtime/streaming surfaces. */
65
+ /**
66
+ * WebSocket close codes used across the PyAI realtime/streaming surfaces.
67
+ * Browser callers may initiate only Normal or values in 3000-4999; the
68
+ * standards-reserved values below are server close observations.
69
+ */
66
70
  export const WSCloseCode = {
67
71
  /** Normal closure. */
68
72
  Normal: 1000,
73
+ /** Browser-safe private application code for a malformed peer protocol frame. */
74
+ ProtocolViolation: 4002,
69
75
  /** Auth/policy: bad key, missing scope, or revoked token. */
70
76
  PolicyViolation: 1008,
71
77
  /** Engine/internal error. */
@@ -92,7 +98,22 @@ export const ErrorCode = {
92
98
  IdempotencyConflict: "idempotency_conflict",
93
99
  NotFound: "not_found",
94
100
  NumberInUse: "number_in_use",
101
+ UnsupportedToolTransport: "unsupported_tool_transport",
95
102
  };
103
+ /**
104
+ * The non-secret marker every PyAI realtime client offers ALONGSIDE its
105
+ * `pyai-key.<KEY>` credential token.
106
+ *
107
+ * A browser cannot set `Authorization` on a WebSocket, so the key rides in
108
+ * `Sec-WebSocket-Protocol`. RFC 6455 makes the server echo the subprotocol it
109
+ * SELECTS, so a server that selects the credential publishes the credential —
110
+ * which api.pyai.com did until 2026-09-07. The edge now echoes only this
111
+ * marker. Offering it is not optional for Node clients: `ws` throws
112
+ * "Server sent no subprotocol" when it offered subprotocols and the 101
113
+ * selected none, and RFC 6455 lets the server select only a value the client
114
+ * actually offered.
115
+ */
116
+ export const REALTIME_SUBPROTOCOL_MARKER = "pyai.v1";
96
117
  /**
97
118
  * A live Hear streaming-STT session. Hides the frame protocol: stream audio
98
119
  * with {@link HearStream.sendAudio}, get `onPartial`/`onFinal`/`onError`
@@ -114,7 +135,7 @@ export class HearStream {
114
135
  if (!WS) {
115
136
  throw new Error("No global WebSocket available; pass options.webSocket (e.g. the `ws` package) to transcriptions.stream()");
116
137
  }
117
- this.ws = new WS(url, [subprotocol]);
138
+ this.ws = new WS(url, [REALTIME_SUBPROTOCOL_MARKER, subprotocol]);
118
139
  this.ws.onopen = () => {
119
140
  opts.onOpen?.();
120
141
  };
@@ -205,7 +226,7 @@ export class AmdStream {
205
226
  if (!WS) {
206
227
  throw new Error("No global WebSocket available; pass options.webSocket (e.g. the `ws` package) to amd.stream()");
207
228
  }
208
- this.ws = new WS(url, [subprotocol]);
229
+ this.ws = new WS(url, [REALTIME_SUBPROTOCOL_MARKER, subprotocol]);
209
230
  this.ws.onopen = () => opts.onOpen?.();
210
231
  this.ws.onmessage = (ev) => this.handleMessage(ev.data);
211
232
  this.ws.onerror = (ev) => opts.onError?.(ev instanceof Error ? ev : new Error("WebSocket error"));
@@ -277,6 +298,8 @@ export const OmniEvent = {
277
298
  Flush: "flush",
278
299
  /** Engine requests a client-loop tool invocation. */
279
300
  ToolCall: "tool_call",
301
+ /** A write tool is waiting for the caller to confirm; it has not run. */
302
+ ToolConfirmationRequired: "tool_confirmation_required",
280
303
  /** Session is closing; see close code. */
281
304
  SessionEnd: "session_end",
282
305
  /** Server fault frame. */
@@ -413,7 +436,7 @@ export class OmniConnection {
413
436
  if (!WS) {
414
437
  throw new Error("No global WebSocket available; pass options.webSocket (e.g. the `ws` package) to omni.connect()");
415
438
  }
416
- this.ws = new WS(url, [subprotocol]);
439
+ this.ws = new WS(url, [REALTIME_SUBPROTOCOL_MARKER, subprotocol]);
417
440
  this.ws.onopen = () => {
418
441
  if (opts.configure) {
419
442
  try {
@@ -679,12 +702,41 @@ export class PyAI {
679
702
  q.set("gender", params.gender);
680
703
  if (params.region)
681
704
  q.set("region", params.region);
705
+ if (params.language)
706
+ q.set("language", params.language);
707
+ if (params.tier)
708
+ q.set("tier", params.tier);
709
+ if (params.q)
710
+ q.set("q", params.q);
682
711
  const qs = q.toString();
683
712
  const page = await this.getJson(`/v1/voices${qs ? `?${qs}` : ""}`);
684
713
  return { ...page, data: page.data.map(normalizeVoice) };
685
714
  },
686
715
  get: async (id) => normalizeVoice(await this.getJson(`/v1/voices/${encodeURIComponent(id)}`)),
687
716
  };
717
+ // --- Hear organization settings ----------------------------------------
718
+ hear = {
719
+ vocabulary: {
720
+ /** Read organization-owned vocabulary. Scope `hear:configure`. */
721
+ get: () => this.getJson("/v1/hear/vocabulary"),
722
+ /** Replace vocabulary and activation profiles. Scope `hear:configure`. */
723
+ set: (input) => this.putJson("/v1/hear/vocabulary", {
724
+ terms: input.terms,
725
+ enabled_for: input.enabledFor,
726
+ }),
727
+ },
728
+ };
729
+ // --- managed Agents ----------------------------------------------------
730
+ agents = {
731
+ list: () => this.getJson("/v1/agents"),
732
+ get: (agentId) => this.getJson(`/v1/agents/${encodeURIComponent(agentId)}`),
733
+ create: (input) => this.postJson("/v1/agents", input),
734
+ update: (agentId, patch) => this.postJson(`/v1/agents/${encodeURIComponent(agentId)}`, patch),
735
+ delete: async (agentId) => {
736
+ const response = await this.deleteReq(`/v1/agents/${encodeURIComponent(agentId)}`);
737
+ return (await response.json());
738
+ },
739
+ };
688
740
  // --- audio --------------------------------------------------------------
689
741
  audio = {
690
742
  /** Text-to-speech. Returns the raw audio bytes (default WAV). */
@@ -700,10 +752,10 @@ export class PyAI {
700
752
  /**
701
753
  * Text-to-speech, streamed. Resolves as soon as the response headers arrive
702
754
  * with the body as a `ReadableStream` of audio bytes, so you can start
703
- * playback or forward the audio at the first chunk (the engine's
704
- * time-to-first-byte is tens of ms) instead of buffering the whole clip.
705
- * Use `mp3` for the smoothest progressive playback. Returns an async
706
- * iterable of Uint8Array chunks.
755
+ * playback or forward the audio at the first chunk. Use `pcm`, `wav`,
756
+ * or G.711 for streaming; `mp3` and `opus` are buffered server-side.
757
+ * Consume the stream immediately and cancel its reader when stopping early.
758
+ * HTTP connection reuse and protocol negotiation are controlled by fetch.
707
759
  */
708
760
  speechStream: async (params) => {
709
761
  assertActiveSpeechParams(params);
@@ -724,6 +776,16 @@ export class PyAI {
724
776
  form.set("model", params.model ?? "pyai-hear");
725
777
  if (params.language)
726
778
  form.set("language", params.language);
779
+ if (params.numerals !== undefined)
780
+ form.set("numerals", String(params.numerals));
781
+ if (params.smart_format !== undefined)
782
+ form.set("smart_format", String(params.smart_format));
783
+ if (params.dictation !== undefined)
784
+ form.set("dictation", String(params.dictation));
785
+ if (params.drop_fillers !== undefined)
786
+ form.set("drop_fillers", String(params.drop_fillers));
787
+ if (params.vocabulary?.length)
788
+ form.set("vocabulary", params.vocabulary.join(","));
727
789
  if (params.response_format)
728
790
  form.set("response_format", params.response_format);
729
791
  if (params.seed !== undefined)
@@ -985,7 +1047,7 @@ export class PyAI {
985
1047
  * realtime. **Call this from your server** with a secret key holding
986
1048
  * `omni:session`; never ship the secret key to a page. Hand the returned
987
1049
  * `token` to the browser, which connects with
988
- * `new WebSocket(session.url, ["pyai-key." + session.token])`. The token
1050
+ * `new WebSocket(session.url, ["pyai.v1", "pyai-key." + session.token])`. The token
989
1051
  * expires after `ttlSeconds` (default 60s) and only works from
990
1052
  * `allowedOrigins`. Scope `omni:session`.
991
1053
  */
@@ -1035,14 +1097,29 @@ export class PyAI {
1035
1097
  const qs = q.toString();
1036
1098
  return `${wsBase}/v1/omni${qs ? `?${qs}` : ""}`;
1037
1099
  }
1038
- /** The subprotocol that carries the key on a WS upgrade (browser-safe auth). */
1100
+ /** The subprotocol token that carries the key on a WS upgrade. */
1039
1101
  realtimeSubprotocol() {
1040
1102
  return `pyai-key.${this.apiKey}`;
1041
1103
  }
1104
+ /**
1105
+ * The full subprotocol list to open a PyAI WebSocket with:
1106
+ * `[marker, credential]`. Always pass BOTH — see
1107
+ * {@link REALTIME_SUBPROTOCOL_MARKER}. Pass `token` to use an ephemeral
1108
+ * session token (from `omni.createSession`) instead of the secret key.
1109
+ */
1110
+ realtimeSubprotocols(token) {
1111
+ return [
1112
+ REALTIME_SUBPROTOCOL_MARKER,
1113
+ token ? `pyai-key.${token}` : this.realtimeSubprotocol(),
1114
+ ];
1115
+ }
1042
1116
  /** Build the Hear streaming-STT WebSocket URL (`/v1/audio/transcriptions/stream`). */
1043
1117
  hearStreamURL(opts = {}) {
1044
1118
  const wsBase = this.baseURL.replace(/^http/, "ws");
1045
1119
  const q = new URLSearchParams(opts.query ?? {});
1120
+ // `context` is private to PyAI's adapter-to-server hop. Never expose or
1121
+ // forward it from the public SDK escape hatch.
1122
+ q.delete("context");
1046
1123
  q.set("protocol", "pyai-hear-v1");
1047
1124
  if (opts.model)
1048
1125
  q.set("model", opts.model);
@@ -1056,6 +1133,15 @@ export class PyAI {
1056
1133
  q.set("interim_results", String(opts.interimResults));
1057
1134
  if (opts.numerals !== undefined)
1058
1135
  q.set("numerals", String(opts.numerals));
1136
+ if (opts.smartFormat !== undefined)
1137
+ q.set("smart_format", String(opts.smartFormat));
1138
+ if (opts.dictation !== undefined)
1139
+ q.set("dictation", String(opts.dictation));
1140
+ if (opts.dropFillers !== undefined)
1141
+ q.set("drop_fillers", String(opts.dropFillers));
1142
+ if (opts.vocabulary?.length) {
1143
+ q.set("vocabulary", JSON.stringify(opts.vocabulary));
1144
+ }
1059
1145
  if (opts.endpointingMs !== undefined)
1060
1146
  q.set("endpointing_ms", String(opts.endpointingMs));
1061
1147
  const qs = q.toString();
@@ -1080,8 +1166,8 @@ export class PyAI {
1080
1166
  connectRealtime(opts = {}) {
1081
1167
  const WS = globalThis.WebSocket;
1082
1168
  if (!WS)
1083
- throw new Error("No global WebSocket; use realtimeURL()/realtimeSubprotocol() with a WS library");
1084
- return new WS(this.realtimeURL(opts), [this.realtimeSubprotocol()]);
1169
+ throw new Error("No global WebSocket; use realtimeURL()/realtimeSubprotocols() with a WS library");
1170
+ return new WS(this.realtimeURL(opts), this.realtimeSubprotocols());
1085
1171
  }
1086
1172
  }
1087
1173
  export default PyAI;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyai/sdk",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Official TypeScript/JavaScript SDK for PyAI, speech-to-text (Hear), text-to-speech (Speak), realtime voice agents (Omni), and call compliance (Trace).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,7 +17,10 @@
17
17
  "files": [
18
18
  "dist",
19
19
  "src",
20
- "README.md"
20
+ "README.md",
21
+ "CLI.md",
22
+ "CLI.schema.json",
23
+ "AGENT_GUIDE.md"
21
24
  ],
22
25
  "repository": {
23
26
  "type": "git",
@@ -28,6 +31,8 @@
28
31
  "bugs": "https://github.com/atomsai/pyai-platform-backend/issues",
29
32
  "author": "PyAI",
30
33
  "scripts": {
34
+ "gen:cli-assets": "node scripts/generate-cli-assets.mjs",
35
+ "check:cli-assets": "node scripts/generate-cli-assets.mjs --check",
31
36
  "build": "tsc -p tsconfig.build.json",
32
37
  "test": "node --test test/**/*.test.ts",
33
38
  "typecheck": "tsc --noEmit"