realtime-avatar 0.24.1 → 0.25.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.
@@ -0,0 +1,143 @@
1
+ import { RealtimeAvatar, ROUTE_TIMEOUT_MS, isQueued, RealtimeAvatarHttpError, RealtimeAvatarError } from './chunk-GMYGHDY2.js';
2
+
3
+ // ../http-client/src/generated/error-semantics.ts
4
+ var ERROR_SEMANTICS = {
5
+ "bad_request": { statuses: [400], message: "Check the request and try again.", retryable: false },
6
+ "unauthorized": { statuses: [401], message: "Authentication is required to use Realtime Avatar.", retryable: false },
7
+ "payment_required": { statuses: [402], message: "Add credits to continue using realtime avatars.", retryable: false },
8
+ "forbidden": { statuses: [403], message: "This API key does not have access to that action.", retryable: false },
9
+ "not_found": { statuses: [404], message: "We could not find that avatar or resource.", retryable: false },
10
+ "method_not_allowed": { statuses: [405], message: "This request method is not supported.", retryable: false },
11
+ "not_acceptable": { statuses: [406], message: "The requested response format is not supported.", retryable: false },
12
+ "request_aborted": { statuses: [408], message: "The session request was cancelled.", retryable: false },
13
+ "conflict": { statuses: [409], message: "That request is already in progress. Try again in a moment.", retryable: true },
14
+ "length_required": { statuses: [411], message: "A request content length is required.", retryable: false },
15
+ "payload_too_large": { statuses: [413], message: "The request is too large.", retryable: false },
16
+ "unprocessable_entity": { statuses: [422], message: "Check the request and try again.", retryable: false },
17
+ "rate_limited": { statuses: [429], message: "Too many realtime requests. Please slow down and retry.", retryable: true },
18
+ "internal_error": { statuses: [500], message: "The session service encountered an error. Try again.", retryable: true },
19
+ "unavailable": { statuses: [503], message: "Realtime Avatar is temporarily unavailable. Try again in a moment.", retryable: true },
20
+ "insufficient_scope": { statuses: [403], message: "This API key is missing a required permission.", retryable: false },
21
+ "loop_pending": { statuses: [409], message: "An avatar loop update is still in progress. Try again shortly.", retryable: true },
22
+ "rate_limit_exceeded": { statuses: [429], message: "Too many realtime requests. Please slow down and retry.", retryable: true },
23
+ "upstream_failed": { statuses: [500, 502, 503], message: "The session service could not complete the request. Try again.", retryable: true },
24
+ "upstream_unreachable": { statuses: [502, 503], message: "The session service could not be reached. Try again.", retryable: true },
25
+ "invalid_upstream_response": { statuses: [502], message: "The session service returned an invalid response. Try again.", retryable: true },
26
+ "upstream_timeout": { statuses: [504], message: "The session service took too long to respond. Try again.", retryable: true },
27
+ "capacity_unavailable": { statuses: [503], message: "Session availability could not be checked. Try again.", retryable: true },
28
+ "session_dispatch_failed": { statuses: [503], message: "The avatar session could not be started. Try again.", retryable: true },
29
+ "admission_unavailable": { statuses: [503], message: "Session admission is temporarily unavailable. Try again.", retryable: true },
30
+ "service_warming": { statuses: [503], message: "Realtime Avatar is warming up. Try again in a moment.", retryable: true },
31
+ "avatar_not_ready": { statuses: [409], message: "This avatar is still being built \u2014 it is usually ready within a minute. Try again then.", retryable: true },
32
+ "clip_library_not_ready": { statuses: [409], message: "This avatar's motion clips are still rendering. Try again in a minute.", retryable: true },
33
+ "recording_unsupported": { statuses: [501], message: "Session recording is not enabled for this deployment.", retryable: false },
34
+ "recording_requires_new_room": { statuses: [422], message: "Recording requires a new session room. Start a fresh session to record it.", retryable: false },
35
+ "recording_unavailable": { statuses: [503], message: "Session recording is temporarily unavailable. Try again in a moment.", retryable: true }
36
+ };
37
+ var MESSAGE_SHAPE_PATTERNS = { "machine": { "source": "[_=<>{}\\[\\]|\\\\$`]|\\w+\\.\\w+\\.\\w|\\S:\\d|\\w\\(\\)|^\\s*(?:at|File)\\b|\\bTraceback \\(most recent call last\\)", "flags": "mu" }, "locator": { "source": "https?://|/[\\w.-]+/|[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}|\\bbearer\\s|[\\r\\n]", "flags": "i" } };
38
+ Object.values(MESSAGE_SHAPE_PATTERNS).map(
39
+ (pattern) => new RegExp(pattern.source, pattern.flags)
40
+ );
41
+
42
+ // ../proxy/src/config.ts
43
+ var MISCONFIGURED = "internal_error";
44
+ var TIMED_OUT = "upstream_timeout";
45
+ var json = (body, status = 200) => new Response(JSON.stringify(body), {
46
+ status,
47
+ headers: { "content-type": "application/json", "cache-control": "no-store" }
48
+ });
49
+ function relayFailure(operation, error) {
50
+ const misconfigured = error.status === 401 || error.status === 403;
51
+ const status = misconfigured ? ERROR_SEMANTICS[MISCONFIGURED].statuses[0] : error.status;
52
+ const verdict = error.retryable === void 0 ? {} : { retryable: error.retryable };
53
+ const body = misconfigured ? { code: MISCONFIGURED, status, retryable: false } : error.status === 429 ? throttled(error) : { code: error.isBilling ? error.code ?? "insufficient_credits" : error.code, status, ...verdict };
54
+ console.error(
55
+ `realtime-avatar: platform answered ${operation} with ${error.status} (code ${error.code ?? "none"}, requestId ${error.requestId ?? "none"}); relayed as ${status}` + (misconfigured ? " \u2014 the platform refused this route's API key" : "")
56
+ );
57
+ const response = json({ ...body, ...error.requestId ? { requestId: error.requestId } : {} }, status);
58
+ if (error.requestId) response.headers.set("X-Request-ID", error.requestId);
59
+ return response;
60
+ }
61
+ function timedOut(operation) {
62
+ const { retryable, statuses: [status] } = ERROR_SEMANTICS[TIMED_OUT];
63
+ console.error(`realtime-avatar: platform did not answer ${operation} inside the route's budget; answered ${status}`);
64
+ return json({ code: TIMED_OUT, status, retryable }, status);
65
+ }
66
+ function throttled(error) {
67
+ const concurrency = error.code === "concurrency_limit_reached";
68
+ const counts = error.concurrency;
69
+ const message = concurrency ? counts?.maxConcurrentSessions !== void 0 && counts.activeSessions !== void 0 && counts.connectingSessions !== void 0 && counts.pendingSessions !== void 0 ? `Session limit reached (${counts.maxConcurrentSessions} allowed): ${counts.activeSessions} active, ${counts.connectingSessions} connecting, ${counts.pendingSessions} starting. End a session or wait for pending starts to clear, then retry.` : "The concurrent session limit is reached. Active and starting sessions count. End a session or wait for pending starts to clear, then retry." : "Too many requests. Wait before retrying.";
70
+ return {
71
+ error: message,
72
+ code: concurrency ? "concurrency_limit_reached" : "rate_limited",
73
+ status: 429,
74
+ retryable: error.retryable ?? true,
75
+ ...concurrency ? counts : {}
76
+ };
77
+ }
78
+ function operationFor(pathname, method) {
79
+ const tail = pathname.replace(/\/+$/, "").split("/").pop() ?? "";
80
+ if (method === "POST" && (tail === "connect" || tail === "call")) return "connect";
81
+ if (method === "POST" && (tail === "end" || tail === "release")) return "end";
82
+ if (method === "GET" && tail === "avatars") return "avatars";
83
+ if (method === "GET" && tail === "credits") return "credits";
84
+ return null;
85
+ }
86
+ function createProxyHandler(config) {
87
+ const minted = /* @__PURE__ */ new Map();
88
+ const MINTED_TTL_MS = 30 * 6e4;
89
+ return async (request) => {
90
+ const url = new URL(request.url);
91
+ const operation = operationFor(url.pathname, request.method);
92
+ if (!operation) return json({ error: "not found" }, 404);
93
+ const refusal = await config.authorize?.({ request, operation });
94
+ if (refusal instanceof Response) return refusal;
95
+ const apiKey = typeof config.apiKey === "function" ? await config.apiKey() : config.apiKey;
96
+ const rta = new RealtimeAvatar({ apiKey, baseUrl: config.baseUrl, totalTimeoutMs: config.timeoutMs ?? ROUTE_TIMEOUT_MS });
97
+ try {
98
+ if (operation === "avatars") return json({ data: await rta.listAvatars() });
99
+ if (operation === "credits") return json(await rta.creditBalance());
100
+ if (operation === "end") {
101
+ const ended = await request.json().catch(() => ({}));
102
+ const sessionId = ended.session_id ?? ended.queue_ticket_id;
103
+ if (!sessionId) return json({ error: "session_id or queue_ticket_id is required" }, 422);
104
+ const owns = config.ownsSession ? await config.ownsSession({ request, sessionId }) : minted.delete(sessionId);
105
+ if (!owns) return new Response(null, { status: 204 });
106
+ const reason = ended.reason === "page_hide" || ended.reason === "unmount" ? ended.reason : "manual";
107
+ await rta.endCall(sessionId, { reason });
108
+ return new Response(null, { status: 204 });
109
+ }
110
+ const body = await request.json().catch(() => ({}));
111
+ if (!body.avatarId) return json({ error: "avatarId is required" }, 422);
112
+ const mode = body.mode === "voice" ? "voice" : "avatar";
113
+ const decided = await config.session?.({ request, avatarId: body.avatarId, mode });
114
+ if (decided instanceof Response) return decided;
115
+ const call = await rta.startCall({ avatarId: body.avatarId, mode, ...decided ?? {} });
116
+ if (isQueued(call)) {
117
+ if (call.queueTicketId) minted.set(call.queueTicketId, Date.now());
118
+ return json(
119
+ {
120
+ queued: true,
121
+ position: call.position,
122
+ size: call.size,
123
+ retryAfterMs: call.retryAfterMs,
124
+ queue_ticket_id: call.queueTicketId
125
+ },
126
+ 429
127
+ );
128
+ }
129
+ const now = Date.now();
130
+ for (const [id, at] of minted) if (now - at > MINTED_TTL_MS) minted.delete(id);
131
+ minted.set(call.sessionId, now);
132
+ return json(call.raw);
133
+ } catch (error) {
134
+ if (error instanceof RealtimeAvatarHttpError) return relayFailure(operation, error);
135
+ if (error instanceof RealtimeAvatarError && error.cause instanceof Error && error.cause.name === "TimeoutError") {
136
+ return timedOut(operation);
137
+ }
138
+ throw error;
139
+ }
140
+ };
141
+ }
142
+
143
+ export { createProxyHandler };
@@ -8,11 +8,16 @@ var RealtimeAvatarError = class extends Error {
8
8
  this.name = "RealtimeAvatarError";
9
9
  }
10
10
  };
11
+ function isRequestId(value) {
12
+ return typeof value === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value);
13
+ }
11
14
  var RealtimeAvatarHttpError = class extends RealtimeAvatarError {
12
15
  status;
13
16
  code;
14
17
  body;
15
18
  requestId;
19
+ /** The platform's own verdict on retrying as-is, when its body gave one. */
20
+ retryable;
16
21
  concurrency;
17
22
  constructor(status, code, body, requestId) {
18
23
  const bounded = body.slice(0, 400);
@@ -27,8 +32,8 @@ var RealtimeAvatarHttpError = class extends RealtimeAvatarError {
27
32
  if (value && typeof value === "object" && !Array.isArray(value)) parsed = value;
28
33
  } catch {
29
34
  }
30
- const validId = (value) => typeof value === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value);
31
- this.requestId = validId(parsed.requestId) ? parsed.requestId : validId(requestId) ? requestId : void 0;
35
+ this.requestId = isRequestId(parsed.requestId) ? parsed.requestId : isRequestId(requestId) ? requestId : void 0;
36
+ if (typeof parsed.retryable === "boolean") this.retryable = parsed.retryable;
32
37
  if (status === 429 && code === "concurrency_limit_reached") {
33
38
  this.concurrency = {};
34
39
  for (const key of ["maxConcurrentSessions", "liveSessions", "activeSessions", "connectingSessions", "pendingSessions"]) {
@@ -293,6 +298,12 @@ function backoffMs(attempt, retryAfter) {
293
298
  return Math.random() * Math.min(500 * 2 ** attempt, 8e3);
294
299
  }
295
300
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
301
+ var PROXY_CLIENT_TIMEOUT_MS = 6e4;
302
+ var ROUTE_TIMEOUT_MS = PROXY_CLIENT_TIMEOUT_MS - 1e4;
303
+ function retryFits(attemptStartedAt, waitMs, deadline) {
304
+ const now = Date.now();
305
+ return now + waitMs + (now - attemptStartedAt) <= deadline;
306
+ }
296
307
  function isTransient(cause) {
297
308
  const name = cause?.name;
298
309
  return name === "TimeoutError" || name === "TypeError" || name === "FetchError";
@@ -654,12 +665,13 @@ z.enum([
654
665
 
655
666
  // ../http-client/src/client.ts
656
667
  var DEFAULT_BASE_URL = "https://realtimeavatar.ai/api/v1";
657
- var SDK_VERSION = "0.24.1";
668
+ var SDK_VERSION = "0.25.0";
658
669
  var RealtimeAvatar = class {
659
670
  #apiKey;
660
671
  #baseUrl;
661
672
  #fetch;
662
673
  #timeoutMs;
674
+ #totalTimeoutMs;
663
675
  #maxRetries;
664
676
  #userAgent;
665
677
  constructor(options) {
@@ -673,6 +685,7 @@ var RealtimeAvatar = class {
673
685
  this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
674
686
  this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
675
687
  this.#timeoutMs = options.timeoutMs ?? 6e4;
688
+ this.#totalTimeoutMs = options.totalTimeoutMs;
676
689
  this.#maxRetries = Math.max(0, options.maxRetries ?? 2);
677
690
  this.#userAgent = [`realtime-avatar-sdk/${SDK_VERSION}`, runtimeTag(), options.userAgent].filter(Boolean).join(" ");
678
691
  }
@@ -1099,24 +1112,29 @@ var RealtimeAvatar = class {
1099
1112
  body = JSON.stringify(init.json);
1100
1113
  }
1101
1114
  if (MUTATING.has(method)) headers["idempotency-key"] = newIdempotencyKey();
1115
+ const deadline = this.#totalTimeoutMs === void 0 ? Infinity : Date.now() + this.#totalTimeoutMs;
1102
1116
  let lastError;
1103
1117
  for (let attempt = 0; ; attempt++) {
1118
+ const startedAt = Date.now();
1104
1119
  try {
1105
- const signal = AbortSignal.timeout(this.#timeoutMs);
1120
+ const signal = AbortSignal.timeout(Math.max(0, Math.min(this.#timeoutMs, deadline - startedAt)));
1106
1121
  const response = await this.#fetch(`${this.#baseUrl}${path}`, { method, headers, body, signal });
1107
1122
  if (attempt >= this.#maxRetries || !RETRYABLE_STATUS.has(response.status)) return response;
1123
+ const wait = backoffMs(attempt, response.headers.get("retry-after"));
1124
+ if (!retryFits(startedAt, wait, deadline)) return response;
1108
1125
  await response.body?.cancel().catch(() => {
1109
1126
  });
1110
- await sleep(backoffMs(attempt, response.headers.get("retry-after")));
1127
+ await sleep(wait);
1111
1128
  } catch (cause) {
1112
1129
  lastError = cause;
1113
- if (attempt >= this.#maxRetries || !isTransient(cause) || isStream(body)) {
1130
+ const wait = backoffMs(attempt, null);
1131
+ if (attempt >= this.#maxRetries || !isTransient(cause) || isStream(body) || !retryFits(startedAt, wait, deadline)) {
1114
1132
  throw new RealtimeAvatarError(
1115
1133
  `${method} ${path} failed after ${attempt + 1} attempt(s): ${cause.message}`,
1116
1134
  { cause: lastError }
1117
1135
  );
1118
1136
  }
1119
- await sleep(backoffMs(attempt, null));
1137
+ await sleep(wait);
1120
1138
  }
1121
1139
  }
1122
1140
  }
@@ -1263,4 +1281,4 @@ function isQueued(result) {
1263
1281
  return "queued" in result;
1264
1282
  }
1265
1283
 
1266
- export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, listRecordingsResponseSchema, recordingAccessResponseSchema, recordingArtifactSchema, recordingModeSchema, verifyTranscript };
1284
+ export { ROUTE_TIMEOUT_MS, RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, listRecordingsResponseSchema, recordingAccessResponseSchema, recordingArtifactSchema, recordingModeSchema, verifyTranscript };
package/dist/express.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { P as ProxyConfig } from './types-CpqMHna3.js';
1
+ import { P as ProxyConfig } from './types-C7B7jOx6.js';
2
2
  import './types-D77jgSms.js';
3
3
  import 'zod';
4
4
 
package/dist/express.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-4PWWEERK.js';
2
- import './chunk-EODL5S3A.js';
1
+ import { createProxyHandler } from './chunk-DNHHQ5GZ.js';
2
+ import './chunk-GMYGHDY2.js';
3
3
  import './chunk-MCIXU6XP.js';
4
4
 
5
5
  // ../proxy/src/express.ts
@@ -16,7 +16,9 @@ function realtimeAvatarExpress(config) {
16
16
  body: req.method === "GET" ? void 0 : JSON.stringify(req.body ?? {})
17
17
  });
18
18
  const response = await handler(request);
19
- res.status(response.status).set("content-type", "application/json").send(await response.text());
19
+ res.status(response.status).set("content-type", "application/json");
20
+ response.headers.forEach((value, field) => res.set(field, value));
21
+ res.send(await response.text());
20
22
  };
21
23
  }
22
24
 
package/dist/hono.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { P as ProxyConfig } from './types-CpqMHna3.js';
1
+ import { P as ProxyConfig } from './types-C7B7jOx6.js';
2
2
  import './types-D77jgSms.js';
3
3
  import 'zod';
4
4
 
package/dist/hono.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-4PWWEERK.js';
2
- import './chunk-EODL5S3A.js';
1
+ import { createProxyHandler } from './chunk-DNHHQ5GZ.js';
2
+ import './chunk-GMYGHDY2.js';
3
3
  import './chunk-MCIXU6XP.js';
4
4
 
5
5
  // ../proxy/src/hono.ts
package/dist/index.d.ts CHANGED
@@ -8,8 +8,17 @@ interface RealtimeAvatarOptions {
8
8
  apiKey: string;
9
9
  baseUrl?: string;
10
10
  fetch?: typeof fetch;
11
- /** Per-request timeout. Default 60s; starting a call does real work upstream. */
11
+ /** Per-attempt timeout. Default 60s; starting a call does real work upstream. */
12
12
  timeoutMs?: number;
13
+ /**
14
+ * The whole request: every attempt and the backoff between them. Default unbounded, so the
15
+ * worst case is `timeoutMs` per attempt plus up to 20s of `Retry-After` between them.
16
+ *
17
+ * Set it when something else is waiting on this request with a deadline of its own (a
18
+ * browser, a serverless max duration). An attempt is cut off at the budget, and a retry is
19
+ * started only if it can take as long as the attempt before it and still finish inside it.
20
+ */
21
+ totalTimeoutMs?: number;
13
22
  /**
14
23
  * Extra attempts after a transient failure. Default 2, so 3 attempts in total.
15
24
  *
@@ -319,6 +328,8 @@ declare class RealtimeAvatarHttpError extends RealtimeAvatarError {
319
328
  readonly code: string | undefined;
320
329
  readonly body: string;
321
330
  readonly requestId?: string;
331
+ /** The platform's own verdict on retrying as-is, when its body gave one. */
332
+ readonly retryable?: boolean;
322
333
  readonly concurrency?: Partial<Record<"maxConcurrentSessions" | "liveSessions" | "activeSessions" | "connectingSessions" | "pendingSessions", number>>;
323
334
  constructor(status: number, code: string | undefined, body: string, requestId?: string);
324
335
  /** Out of credits, or over this key's spend limit. Surface a paywall, not an error. */
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, listRecordingsResponseSchema, recordingAccessResponseSchema, recordingArtifactSchema, recordingModeSchema, verifyTranscript } from './chunk-EODL5S3A.js';
1
+ export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, listRecordingsResponseSchema, recordingAccessResponseSchema, recordingArtifactSchema, recordingModeSchema, verifyTranscript } from './chunk-GMYGHDY2.js';
2
2
  import './chunk-MCIXU6XP.js';
package/dist/nextjs.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { P as ProxyConfig } from './types-CpqMHna3.js';
1
+ import { P as ProxyConfig } from './types-C7B7jOx6.js';
2
2
  import './types-D77jgSms.js';
3
3
  import 'zod';
4
4
 
package/dist/nextjs.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-4PWWEERK.js';
2
- import './chunk-EODL5S3A.js';
1
+ import { createProxyHandler } from './chunk-DNHHQ5GZ.js';
2
+ import './chunk-GMYGHDY2.js';
3
3
  import './chunk-MCIXU6XP.js';
4
4
 
5
5
  // ../proxy/src/nextjs.ts
@@ -1858,7 +1858,11 @@ interface ProxyClientOptions {
1858
1858
  /** Swap the transport — a test double, or a fetch that carries your session cookie. */
1859
1859
  fetch?: typeof globalThis.fetch;
1860
1860
  /**
1861
- * Per-request deadline, default 60s, `0` to disable.
1861
+ * Per-request deadline, default 60s, `0` to disable. Keep it above your route's `timeoutMs`
1862
+ * (default 50s), so the route's answer arrives before the page stops listening. Running out throws a
1863
+ * `RealtimeAvatarApiError` with `code: "upstream_timeout"` (status 504, `response: null`).
1864
+ * Do not re-send a timed-out `connect` automatically: your route may still be minting it, and
1865
+ * this client never retries a mint — the server client inside your route owns that retry.
1862
1866
  *
1863
1867
  * Not optional in spirit: a proxy that accepts the connection and then never answers leaves
1864
1868
  * a promise that never settles, which presents as a page stuck on "connecting" with no error
@@ -1,7 +1,7 @@
1
1
  import { LiveKitRoomProps } from '@livekit/react-native';
2
2
  export { AudioSession, VideoTrack, VideoTrackProps, registerGlobals } from '@livekit/react-native';
3
- import { L as LiveKitSessionGrant, A as AvatarVideoFit } from './proxy-client-DlFXaXtn.js';
4
- export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, g as AvatarConnectionDetails, h as AvatarSessionClient, B as BehaviorSnapshot, C as CallTranscript, i as CallTranscriptSegment, j as CartesiaTtsModel, k as ClipResult, l as ClosingTurnResult, m as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, n as DEFAULT_CREDITS_LOW_LEAD_SECONDS, o as DEFAULT_GOVERNOR_CONFIG, p as DEFAULT_GRACE_CEILING_SECONDS, q as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, r as DEFAULT_IDLE_SECONDS, s as DEFAULT_IDLE_WARN_LEAD_SECONDS, t as DEFAULT_TURN_TIMEOUT_SECONDS, u as DeclaredInputSource, E as EndReason, v as EndedEvent, w as ExtendResult, F as FishTtsModel, x as FreezeReadingFn, G as Governor, y as GovernorAction, z as GovernorConfig, H as GovernorSignal, I as GovernorState, J as GraceWindowClosedEvent, K as GraceWindowOpenEvent, M as GraceWindowState, N as IdleWarningEvent, O as InboundRtpCursor, P as InboundRtpReading, Q as KnownBehaviorState, R as LLMProvider, S as LLMSelection, T as LiveKitAvatarGrantState, U as LiveKitAvatarGrantStatus, V as LiveKitCapacityState, W as LiveKitConnectionStatus, X as LiveKitSessionRequest, Y as LiveKitSessionStartResult, Z as MAX_RECONNECT_ATTEMPTS, _ as MAX_SESSION_INSTRUCTIONS_CHARS, $ as ProxyClientOptions, a0 as QualityCap, a1 as RECONNECT_BACKOFF_MS, a2 as RealtimeAvatarRequestOptions, a3 as RealtimeSessionApi, a4 as RealtimeSessionMedia, a5 as RealtimeSessionRoomSinks, a6 as ReconnectPolicy, a7 as ReconnectingEvent, a8 as RecoveryState, a9 as RetryStep, aa as SendTurnOptions, ab as SessionBehavior, ac as SessionClip, ad as SessionClocks, ae as SessionEndReason, af as SessionLifecycleApi, ag as SessionLifecyclePhase, ah as SessionLifecyclePhaseKind, ai as SessionLifecycleRoomBridge, aj as SessionLifecycleRoomBridgeProps, ak as SurfaceLayers, al as TranscriptSender, am as TranscriptSenderOptions, an as TurnState, ao as TurnTimeoutEvent, ap as UseAvatarQualityGovernorInput, aq as UseLiveKitAvatarGrantInput, ar as UseRealtimeSessionInput, as as UseSessionLifecycleInput, at as VoiceSpec, au as VoiceSpecInput, av as capacityErrorFromBusy, aw as capacityStateFromGrant, ax as createProxyClient, ay as isNativeLiveTrackSubscribed, az as knownBehaviorStates, aA as mapTurnState, aB as readInboundRtp, aC as sessionBehaviorSchema, aD as sessionClipSchema, aE as splitCallTranscript, aF as useAvatarAdaptivePlayoutDelay, aG as useAvatarCamera, aH as useAvatarPlayoutDelay, aI as useAvatarQualityGovernor, aJ as useCallTranscript, aK as useLiveKitAvatarGrant, aL as useLiveTrackProducing, aM as useMicLease, aN as useRealtimeSession, aO as useReleaseMicLeaseOnTrackEnded, aP as useSessionLifecycle } from './proxy-client-DlFXaXtn.js';
3
+ import { L as LiveKitSessionGrant, A as AvatarVideoFit } from './proxy-client-O-Mu9JKt.js';
4
+ export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, g as AvatarConnectionDetails, h as AvatarSessionClient, B as BehaviorSnapshot, C as CallTranscript, i as CallTranscriptSegment, j as CartesiaTtsModel, k as ClipResult, l as ClosingTurnResult, m as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, n as DEFAULT_CREDITS_LOW_LEAD_SECONDS, o as DEFAULT_GOVERNOR_CONFIG, p as DEFAULT_GRACE_CEILING_SECONDS, q as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, r as DEFAULT_IDLE_SECONDS, s as DEFAULT_IDLE_WARN_LEAD_SECONDS, t as DEFAULT_TURN_TIMEOUT_SECONDS, u as DeclaredInputSource, E as EndReason, v as EndedEvent, w as ExtendResult, F as FishTtsModel, x as FreezeReadingFn, G as Governor, y as GovernorAction, z as GovernorConfig, H as GovernorSignal, I as GovernorState, J as GraceWindowClosedEvent, K as GraceWindowOpenEvent, M as GraceWindowState, N as IdleWarningEvent, O as InboundRtpCursor, P as InboundRtpReading, Q as KnownBehaviorState, R as LLMProvider, S as LLMSelection, T as LiveKitAvatarGrantState, U as LiveKitAvatarGrantStatus, V as LiveKitCapacityState, W as LiveKitConnectionStatus, X as LiveKitSessionRequest, Y as LiveKitSessionStartResult, Z as MAX_RECONNECT_ATTEMPTS, _ as MAX_SESSION_INSTRUCTIONS_CHARS, $ as ProxyClientOptions, a0 as QualityCap, a1 as RECONNECT_BACKOFF_MS, a2 as RealtimeAvatarRequestOptions, a3 as RealtimeSessionApi, a4 as RealtimeSessionMedia, a5 as RealtimeSessionRoomSinks, a6 as ReconnectPolicy, a7 as ReconnectingEvent, a8 as RecoveryState, a9 as RetryStep, aa as SendTurnOptions, ab as SessionBehavior, ac as SessionClip, ad as SessionClocks, ae as SessionEndReason, af as SessionLifecycleApi, ag as SessionLifecyclePhase, ah as SessionLifecyclePhaseKind, ai as SessionLifecycleRoomBridge, aj as SessionLifecycleRoomBridgeProps, ak as SurfaceLayers, al as TranscriptSender, am as TranscriptSenderOptions, an as TurnState, ao as TurnTimeoutEvent, ap as UseAvatarQualityGovernorInput, aq as UseLiveKitAvatarGrantInput, ar as UseRealtimeSessionInput, as as UseSessionLifecycleInput, at as VoiceSpec, au as VoiceSpecInput, av as capacityErrorFromBusy, aw as capacityStateFromGrant, ax as createProxyClient, ay as isNativeLiveTrackSubscribed, az as knownBehaviorStates, aA as mapTurnState, aB as readInboundRtp, aC as sessionBehaviorSchema, aD as sessionClipSchema, aE as splitCallTranscript, aF as useAvatarAdaptivePlayoutDelay, aG as useAvatarCamera, aH as useAvatarPlayoutDelay, aI as useAvatarQualityGovernor, aJ as useCallTranscript, aK as useLiveKitAvatarGrant, aL as useLiveTrackProducing, aM as useMicLease, aN as useRealtimeSession, aO as useReleaseMicLeaseOnTrackEnded, aP as useSessionLifecycle } from './proxy-client-O-Mu9JKt.js';
5
5
  import { ReactNode, ReactElement } from 'react';
6
6
  import { StyleProp, ViewStyle } from 'react-native';
7
7
  export { useChat, useConnectionState, useLocalParticipant, useRoomContext, useTrackToggle, useTranscriptions, useVoiceAssistant } from '@livekit/components-react';
@@ -414,7 +414,13 @@ function normalizeError(input, overrides) {
414
414
  };
415
415
  }
416
416
 
417
+ // ../http-client/src/errors.ts
418
+ function isRequestId(value) {
419
+ return typeof value === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value);
420
+ }
421
+
417
422
  // ../client/src/errors.ts
423
+ var TIMEOUT_CODE = "upstream_timeout";
418
424
  var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
419
425
  constructor(message, status, body, response, metadata = {}) {
420
426
  super(message);
@@ -426,6 +432,7 @@ var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
426
432
  this.retryable = metadata.retryable;
427
433
  this.billingUrl = metadata.billingUrl;
428
434
  this.rawMessage = metadata.rawMessage ?? null;
435
+ this.requestId = metadata.requestId;
429
436
  }
430
437
  status;
431
438
  body;
@@ -435,6 +442,8 @@ var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
435
442
  billingUrl;
436
443
  /** Debug detail returned by the API. Do not render directly in user-facing UI. */
437
444
  rawMessage;
445
+ /** The platform's correlation ID for this attempt. Quote it to support. */
446
+ requestId;
438
447
  get isBillingRequired() {
439
448
  return this.status === 402 || this.code === "insufficient_credits" || this.code === "spend_limit_exceeded";
440
449
  }
@@ -443,14 +452,31 @@ var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
443
452
  const body = contentType.includes("application/json") ? await response.clone().json().catch(() => null) : await response.clone().text().catch(() => "");
444
453
  const rawMessage = (extractErrorMessage(body) ?? response.statusText) || null;
445
454
  const metadata = extractErrorMetadata(body, response.status, rawMessage);
455
+ const bodyRequestId = readRecord(body)?.requestId;
456
+ const headerRequestId = response.headers.get("x-request-id");
446
457
  return new _RealtimeAvatarApiError(
447
458
  metadata.message,
448
459
  response.status,
449
460
  body,
450
461
  response,
451
- metadata
462
+ {
463
+ ...metadata,
464
+ requestId: isRequestId(bodyRequestId) ? bodyRequestId : isRequestId(headerRequestId) ? headerRequestId : void 0
465
+ }
452
466
  );
453
467
  }
468
+ /**
469
+ * A request that got no answer before its deadline. The status is the one the vocabulary pairs
470
+ * with the code, so `normalizeRealtimeAvatarError` reads it back as the same failure.
471
+ */
472
+ static timeout(timeoutMs) {
473
+ const { message, retryable, statuses: [status] } = ERROR_SEMANTICS[TIMEOUT_CODE];
474
+ return new _RealtimeAvatarApiError(message, status, null, null, {
475
+ code: TIMEOUT_CODE,
476
+ retryable,
477
+ rawMessage: `No response within ${timeoutMs}ms.`
478
+ });
479
+ }
454
480
  };
455
481
  var RealtimeAvatarCapacityError = class extends Error {
456
482
  constructor(message, busy) {
@@ -3270,26 +3296,30 @@ function useRealtimeSession(input) {
3270
3296
  );
3271
3297
  }
3272
3298
 
3299
+ // ../http-client/src/retry.ts
3300
+ var PROXY_CLIENT_TIMEOUT_MS = 6e4;
3301
+
3273
3302
  // ../client/src/proxy-client.ts
3274
- var DEFAULT_TIMEOUT_MS = 6e4;
3275
3303
  var normalize = (url) => url.replace(/\/+$/, "");
3276
3304
  function createProxyClient(options) {
3277
3305
  const base = normalize(options.proxyUrl);
3278
3306
  const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
3279
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
3280
- const deadline = (caller) => {
3281
- if (!timeoutMs) return caller;
3282
- const timer = AbortSignal.timeout(timeoutMs);
3283
- return caller ? AbortSignal.any([caller, timer]) : timer;
3284
- };
3307
+ const timeoutMs = options.timeoutMs ?? PROXY_CLIENT_TIMEOUT_MS;
3285
3308
  const post = async (path, body, request) => {
3286
3309
  if (!doFetch) throw new Error("realtime-avatar: no fetch available \u2014 pass one via `fetch`.");
3287
- return doFetch(`${base}${path}`, {
3288
- method: "POST",
3289
- headers: { "content-type": "application/json", ...request?.headers ?? {} },
3290
- body: JSON.stringify(body),
3291
- signal: deadline(request?.signal)
3292
- });
3310
+ const caller = request?.signal;
3311
+ const timer = timeoutMs ? AbortSignal.timeout(timeoutMs) : void 0;
3312
+ try {
3313
+ return await doFetch(`${base}${path}`, {
3314
+ method: "POST",
3315
+ headers: { "content-type": "application/json", ...request?.headers ?? {} },
3316
+ body: JSON.stringify(body),
3317
+ signal: timer && caller ? AbortSignal.any([caller, timer]) : timer ?? caller
3318
+ });
3319
+ } catch (cause) {
3320
+ if (timer?.aborted && !caller?.aborted) throw RealtimeAvatarApiError.timeout(timeoutMs);
3321
+ throw cause;
3322
+ }
3293
3323
  };
3294
3324
  const beacon = (path, body) => {
3295
3325
  const send = globalThis.navigator?.sendBeacon?.bind(globalThis.navigator);
package/dist/react.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { RegisteredTool, ToolRegistration } from './tools.js';
2
- import { aj as SessionLifecycleRoomBridgeProps, h as AvatarSessionClient, A as AvatarVideoFit } from './proxy-client-DlFXaXtn.js';
3
- export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, g as AvatarConnectionDetails, aQ as AvatarVideoSurface, aR as AvatarVideoSurfaceProps, B as BehaviorSnapshot, C as CallTranscript, i as CallTranscriptSegment, j as CartesiaTtsModel, k as ClipResult, l as ClosingTurnResult, m as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, n as DEFAULT_CREDITS_LOW_LEAD_SECONDS, o as DEFAULT_GOVERNOR_CONFIG, p as DEFAULT_GRACE_CEILING_SECONDS, q as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, r as DEFAULT_IDLE_SECONDS, s as DEFAULT_IDLE_WARN_LEAD_SECONDS, t as DEFAULT_TURN_TIMEOUT_SECONDS, u as DeclaredInputSource, E as EndReason, v as EndedEvent, w as ExtendResult, F as FishTtsModel, x as FreezeReadingFn, G as Governor, y as GovernorAction, z as GovernorConfig, H as GovernorSignal, I as GovernorState, J as GraceWindowClosedEvent, K as GraceWindowOpenEvent, M as GraceWindowState, N as IdleWarningEvent, O as InboundRtpCursor, P as InboundRtpReading, Q as KnownBehaviorState, R as LLMProvider, S as LLMSelection, T as LiveKitAvatarGrantState, U as LiveKitAvatarGrantStatus, V as LiveKitCapacityState, W as LiveKitConnectionStatus, X as LiveKitSessionRequest, Y as LiveKitSessionStartResult, aS as LivePlaybackKeeper, Z as MAX_RECONNECT_ATTEMPTS, _ as MAX_SESSION_INSTRUCTIONS_CHARS, aT as MIC_LEASE_ENDED_TIMEOUT_MS, aU as MicLease, aV as NETWORK_QUALITY_POLICY, aW as NetworkQualityStatus, aX as PlayableVideoElement, $ as ProxyClientOptions, a0 as QualityCap, a1 as RECONNECT_BACKOFF_MS, aY as RealtimeAvatarCapacityError, aZ as RealtimeAvatarLiveKitRoom, a_ as RealtimeAvatarLiveKitRoomProps, a2 as RealtimeAvatarRequestOptions, a3 as RealtimeSessionApi, a4 as RealtimeSessionMedia, a5 as RealtimeSessionRoomSinks, a6 as ReconnectPolicy, a7 as ReconnectingEvent, a8 as RecoveryState, a9 as RetryStep, aa as SendTurnOptions, ab as SessionBehavior, ac as SessionClip, ad as SessionClocks, ae as SessionEndReason, af as SessionLifecycleApi, ag as SessionLifecyclePhase, ah as SessionLifecyclePhaseKind, ai as SessionLifecycleRoomBridge, ak as SurfaceLayers, al as TranscriptSender, am as TranscriptSenderOptions, an as TurnState, ao as TurnTimeoutEvent, ap as UseAvatarQualityGovernorInput, aq as UseLiveKitAvatarGrantInput, ar as UseRealtimeSessionInput, as as UseSessionLifecycleInput, at as VoiceSpec, au as VoiceSpecInput, av as capacityErrorFromBusy, aw as capacityStateFromGrant, ax as createProxyClient, az as knownBehaviorStates, aA as mapTurnState, aB as readInboundRtp, aC as sessionBehaviorSchema, aD as sessionClipSchema, a$ as shouldReplayPendingTurn, aE as splitCallTranscript, aF as useAvatarAdaptivePlayoutDelay, aG as useAvatarCamera, aH as useAvatarPlayoutDelay, aI as useAvatarQualityGovernor, aJ as useCallTranscript, aK as useLiveKitAvatarGrant, aM as useMicLease, aN as useRealtimeSession, aO as useReleaseMicLeaseOnTrackEnded, aP as useSessionLifecycle } from './proxy-client-DlFXaXtn.js';
2
+ import { aj as SessionLifecycleRoomBridgeProps, h as AvatarSessionClient, A as AvatarVideoFit } from './proxy-client-O-Mu9JKt.js';
3
+ export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, g as AvatarConnectionDetails, aQ as AvatarVideoSurface, aR as AvatarVideoSurfaceProps, B as BehaviorSnapshot, C as CallTranscript, i as CallTranscriptSegment, j as CartesiaTtsModel, k as ClipResult, l as ClosingTurnResult, m as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, n as DEFAULT_CREDITS_LOW_LEAD_SECONDS, o as DEFAULT_GOVERNOR_CONFIG, p as DEFAULT_GRACE_CEILING_SECONDS, q as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, r as DEFAULT_IDLE_SECONDS, s as DEFAULT_IDLE_WARN_LEAD_SECONDS, t as DEFAULT_TURN_TIMEOUT_SECONDS, u as DeclaredInputSource, E as EndReason, v as EndedEvent, w as ExtendResult, F as FishTtsModel, x as FreezeReadingFn, G as Governor, y as GovernorAction, z as GovernorConfig, H as GovernorSignal, I as GovernorState, J as GraceWindowClosedEvent, K as GraceWindowOpenEvent, M as GraceWindowState, N as IdleWarningEvent, O as InboundRtpCursor, P as InboundRtpReading, Q as KnownBehaviorState, R as LLMProvider, S as LLMSelection, T as LiveKitAvatarGrantState, U as LiveKitAvatarGrantStatus, V as LiveKitCapacityState, W as LiveKitConnectionStatus, X as LiveKitSessionRequest, Y as LiveKitSessionStartResult, aS as LivePlaybackKeeper, Z as MAX_RECONNECT_ATTEMPTS, _ as MAX_SESSION_INSTRUCTIONS_CHARS, aT as MIC_LEASE_ENDED_TIMEOUT_MS, aU as MicLease, aV as NETWORK_QUALITY_POLICY, aW as NetworkQualityStatus, aX as PlayableVideoElement, $ as ProxyClientOptions, a0 as QualityCap, a1 as RECONNECT_BACKOFF_MS, aY as RealtimeAvatarCapacityError, aZ as RealtimeAvatarLiveKitRoom, a_ as RealtimeAvatarLiveKitRoomProps, a2 as RealtimeAvatarRequestOptions, a3 as RealtimeSessionApi, a4 as RealtimeSessionMedia, a5 as RealtimeSessionRoomSinks, a6 as ReconnectPolicy, a7 as ReconnectingEvent, a8 as RecoveryState, a9 as RetryStep, aa as SendTurnOptions, ab as SessionBehavior, ac as SessionClip, ad as SessionClocks, ae as SessionEndReason, af as SessionLifecycleApi, ag as SessionLifecyclePhase, ah as SessionLifecyclePhaseKind, ai as SessionLifecycleRoomBridge, ak as SurfaceLayers, al as TranscriptSender, am as TranscriptSenderOptions, an as TurnState, ao as TurnTimeoutEvent, ap as UseAvatarQualityGovernorInput, aq as UseLiveKitAvatarGrantInput, ar as UseRealtimeSessionInput, as as UseSessionLifecycleInput, at as VoiceSpec, au as VoiceSpecInput, av as capacityErrorFromBusy, aw as capacityStateFromGrant, ax as createProxyClient, az as knownBehaviorStates, aA as mapTurnState, aB as readInboundRtp, aC as sessionBehaviorSchema, aD as sessionClipSchema, a$ as shouldReplayPendingTurn, aE as splitCallTranscript, aF as useAvatarAdaptivePlayoutDelay, aG as useAvatarCamera, aH as useAvatarPlayoutDelay, aI as useAvatarQualityGovernor, aJ as useCallTranscript, aK as useLiveKitAvatarGrant, aM as useMicLease, aN as useRealtimeSession, aO as useReleaseMicLeaseOnTrackEnded, aP as useSessionLifecycle } from './proxy-client-O-Mu9JKt.js';
4
4
  import { ReactNode } from 'react';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
6
  import { R as RecordingArtifact } from './recording-DjswTx5B.js';
package/dist/react.js CHANGED
@@ -568,7 +568,13 @@ function normalizeError(input, overrides) {
568
568
  };
569
569
  }
570
570
 
571
+ // ../http-client/src/errors.ts
572
+ function isRequestId(value) {
573
+ return typeof value === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value);
574
+ }
575
+
571
576
  // ../client/src/errors.ts
577
+ var TIMEOUT_CODE = "upstream_timeout";
572
578
  var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
573
579
  constructor(message, status, body, response, metadata = {}) {
574
580
  super(message);
@@ -580,6 +586,7 @@ var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
580
586
  this.retryable = metadata.retryable;
581
587
  this.billingUrl = metadata.billingUrl;
582
588
  this.rawMessage = metadata.rawMessage ?? null;
589
+ this.requestId = metadata.requestId;
583
590
  }
584
591
  status;
585
592
  body;
@@ -589,6 +596,8 @@ var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
589
596
  billingUrl;
590
597
  /** Debug detail returned by the API. Do not render directly in user-facing UI. */
591
598
  rawMessage;
599
+ /** The platform's correlation ID for this attempt. Quote it to support. */
600
+ requestId;
592
601
  get isBillingRequired() {
593
602
  return this.status === 402 || this.code === "insufficient_credits" || this.code === "spend_limit_exceeded";
594
603
  }
@@ -597,14 +606,31 @@ var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
597
606
  const body = contentType.includes("application/json") ? await response.clone().json().catch(() => null) : await response.clone().text().catch(() => "");
598
607
  const rawMessage = (extractErrorMessage(body) ?? response.statusText) || null;
599
608
  const metadata = extractErrorMetadata(body, response.status, rawMessage);
609
+ const bodyRequestId = readRecord(body)?.requestId;
610
+ const headerRequestId = response.headers.get("x-request-id");
600
611
  return new _RealtimeAvatarApiError(
601
612
  metadata.message,
602
613
  response.status,
603
614
  body,
604
615
  response,
605
- metadata
616
+ {
617
+ ...metadata,
618
+ requestId: isRequestId(bodyRequestId) ? bodyRequestId : isRequestId(headerRequestId) ? headerRequestId : void 0
619
+ }
606
620
  );
607
621
  }
622
+ /**
623
+ * A request that got no answer before its deadline. The status is the one the vocabulary pairs
624
+ * with the code, so `normalizeRealtimeAvatarError` reads it back as the same failure.
625
+ */
626
+ static timeout(timeoutMs) {
627
+ const { message, retryable, statuses: [status] } = ERROR_SEMANTICS[TIMEOUT_CODE];
628
+ return new _RealtimeAvatarApiError(message, status, null, null, {
629
+ code: TIMEOUT_CODE,
630
+ retryable,
631
+ rawMessage: `No response within ${timeoutMs}ms.`
632
+ });
633
+ }
608
634
  };
609
635
  var RealtimeAvatarCapacityError = class extends Error {
610
636
  constructor(message, busy) {
@@ -3896,26 +3922,30 @@ function AvatarCall(props) {
3896
3922
  return view;
3897
3923
  }
3898
3924
 
3925
+ // ../http-client/src/retry.ts
3926
+ var PROXY_CLIENT_TIMEOUT_MS = 6e4;
3927
+
3899
3928
  // ../client/src/proxy-client.ts
3900
- var DEFAULT_TIMEOUT_MS = 6e4;
3901
3929
  var normalize = (url) => url.replace(/\/+$/, "");
3902
3930
  function createProxyClient(options) {
3903
3931
  const base = normalize(options.proxyUrl);
3904
3932
  const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
3905
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
3906
- const deadline = (caller) => {
3907
- if (!timeoutMs) return caller;
3908
- const timer = AbortSignal.timeout(timeoutMs);
3909
- return caller ? AbortSignal.any([caller, timer]) : timer;
3910
- };
3933
+ const timeoutMs = options.timeoutMs ?? PROXY_CLIENT_TIMEOUT_MS;
3911
3934
  const post = async (path, body, request) => {
3912
3935
  if (!doFetch) throw new Error("realtime-avatar: no fetch available \u2014 pass one via `fetch`.");
3913
- return doFetch(`${base}${path}`, {
3914
- method: "POST",
3915
- headers: { "content-type": "application/json", ...request?.headers ?? {} },
3916
- body: JSON.stringify(body),
3917
- signal: deadline(request?.signal)
3918
- });
3936
+ const caller = request?.signal;
3937
+ const timer = timeoutMs ? AbortSignal.timeout(timeoutMs) : void 0;
3938
+ try {
3939
+ return await doFetch(`${base}${path}`, {
3940
+ method: "POST",
3941
+ headers: { "content-type": "application/json", ...request?.headers ?? {} },
3942
+ body: JSON.stringify(body),
3943
+ signal: timer && caller ? AbortSignal.any([caller, timer]) : timer ?? caller
3944
+ });
3945
+ } catch (cause) {
3946
+ if (timer?.aborted && !caller?.aborted) throw RealtimeAvatarApiError.timeout(timeoutMs);
3947
+ throw cause;
3948
+ }
3919
3949
  };
3920
3950
  const beacon = (path, body) => {
3921
3951
  const send = globalThis.navigator?.sendBeacon?.bind(globalThis.navigator);
package/dist/server.js CHANGED
@@ -1,2 +1,2 @@
1
- export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, listRecordingsResponseSchema, recordingAccessResponseSchema, recordingArtifactSchema, recordingModeSchema, verifyTranscript } from './chunk-EODL5S3A.js';
1
+ export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, listRecordingsResponseSchema, recordingAccessResponseSchema, recordingArtifactSchema, recordingModeSchema, verifyTranscript } from './chunk-GMYGHDY2.js';
2
2
  import './chunk-MCIXU6XP.js';
@@ -1,4 +1,4 @@
1
- import { P as ProxyConfig } from './types-CpqMHna3.js';
1
+ import { P as ProxyConfig } from './types-C7B7jOx6.js';
2
2
  import './types-D77jgSms.js';
3
3
  import 'zod';
4
4
 
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-4PWWEERK.js';
2
- import './chunk-EODL5S3A.js';
1
+ import { createProxyHandler } from './chunk-DNHHQ5GZ.js';
2
+ import './chunk-GMYGHDY2.js';
3
3
  import './chunk-MCIXU6XP.js';
4
4
 
5
5
  // src/tanstack-start.ts
@@ -16,6 +16,13 @@ interface ProxyConfig {
16
16
  /** Server-only. `tic_live_…` / `tic_test_…`. */
17
17
  apiKey: string | (() => string | Promise<string>);
18
18
  baseUrl?: string;
19
+ /**
20
+ * How long this route spends on one platform request, every retry and backoff included.
21
+ * Default 50s, below `createProxyClient`'s default 60s wait so the page hears the answer. Raise
22
+ * the browser's `timeoutMs` if you raise this, and keep it under your host's max function
23
+ * duration. Running out answers `504` `upstream_timeout`.
24
+ */
25
+ timeoutMs?: number;
19
26
  /**
20
27
  * Who may do this. Return a `Response` to refuse, or nothing to allow.
21
28
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "realtime-avatar",
3
- "version": "0.24.1",
3
+ "version": "0.25.0",
4
4
  "description": "Realtime Avatar SDK. `realtime-avatar` and the route adapters hold your API key and are server-only; `realtime-avatar/react`, `/browser` and `/tools` never can — importing a server entry into a browser build throws at bundle time.",
5
5
  "license": "MIT",
6
6
  "author": "The Influence Company",
@@ -1,87 +0,0 @@
1
- import { RealtimeAvatar, isQueued, RealtimeAvatarHttpError } from './chunk-EODL5S3A.js';
2
-
3
- // ../proxy/src/config.ts
4
- var json = (body, status = 200) => new Response(JSON.stringify(body), {
5
- status,
6
- headers: { "content-type": "application/json", "cache-control": "no-store" }
7
- });
8
- function operationFor(pathname, method) {
9
- const tail = pathname.replace(/\/+$/, "").split("/").pop() ?? "";
10
- if (method === "POST" && (tail === "connect" || tail === "call")) return "connect";
11
- if (method === "POST" && (tail === "end" || tail === "release")) return "end";
12
- if (method === "GET" && tail === "avatars") return "avatars";
13
- if (method === "GET" && tail === "credits") return "credits";
14
- return null;
15
- }
16
- function createProxyHandler(config) {
17
- const minted = /* @__PURE__ */ new Map();
18
- const MINTED_TTL_MS = 30 * 6e4;
19
- return async (request) => {
20
- const url = new URL(request.url);
21
- const operation = operationFor(url.pathname, request.method);
22
- if (!operation) return json({ error: "not found" }, 404);
23
- const refusal = await config.authorize?.({ request, operation });
24
- if (refusal instanceof Response) return refusal;
25
- const apiKey = typeof config.apiKey === "function" ? await config.apiKey() : config.apiKey;
26
- const rta = new RealtimeAvatar({ apiKey, baseUrl: config.baseUrl });
27
- try {
28
- if (operation === "avatars") return json({ data: await rta.listAvatars() });
29
- if (operation === "credits") return json(await rta.creditBalance());
30
- if (operation === "end") {
31
- const ended = await request.json().catch(() => ({}));
32
- const sessionId = ended.session_id ?? ended.queue_ticket_id;
33
- if (!sessionId) return json({ error: "session_id or queue_ticket_id is required" }, 422);
34
- const owns = config.ownsSession ? await config.ownsSession({ request, sessionId }) : minted.delete(sessionId);
35
- if (!owns) return new Response(null, { status: 204 });
36
- const reason = ended.reason === "page_hide" || ended.reason === "unmount" ? ended.reason : "manual";
37
- await rta.endCall(sessionId, { reason });
38
- return new Response(null, { status: 204 });
39
- }
40
- const body = await request.json().catch(() => ({}));
41
- if (!body.avatarId) return json({ error: "avatarId is required" }, 422);
42
- const mode = body.mode === "voice" ? "voice" : "avatar";
43
- const decided = await config.session?.({ request, avatarId: body.avatarId, mode });
44
- if (decided instanceof Response) return decided;
45
- const call = await rta.startCall({ avatarId: body.avatarId, mode, ...decided ?? {} });
46
- if (isQueued(call)) {
47
- if (call.queueTicketId) minted.set(call.queueTicketId, Date.now());
48
- return json(
49
- {
50
- queued: true,
51
- position: call.position,
52
- size: call.size,
53
- retryAfterMs: call.retryAfterMs,
54
- queue_ticket_id: call.queueTicketId
55
- },
56
- 429
57
- );
58
- }
59
- const now = Date.now();
60
- for (const [id, at] of minted) if (now - at > MINTED_TTL_MS) minted.delete(id);
61
- minted.set(call.sessionId, now);
62
- return json(call.raw);
63
- } catch (error) {
64
- if (error instanceof RealtimeAvatarHttpError && error.status === 429) {
65
- const concurrency = error.code === "concurrency_limit_reached";
66
- const counts = error.concurrency;
67
- const message = concurrency ? counts?.maxConcurrentSessions !== void 0 && counts.activeSessions !== void 0 && counts.connectingSessions !== void 0 && counts.pendingSessions !== void 0 ? `Session limit reached (${counts.maxConcurrentSessions} allowed): ${counts.activeSessions} active, ${counts.connectingSessions} connecting, ${counts.pendingSessions} starting. End a session or wait for pending starts to clear, then retry.` : "The concurrent session limit is reached. Active and starting sessions count. End a session or wait for pending starts to clear, then retry." : "Too many requests. Wait before retrying.";
68
- const response = json({
69
- error: message,
70
- code: concurrency ? "concurrency_limit_reached" : "rate_limited",
71
- status: 429,
72
- retryable: true,
73
- ...concurrency ? counts : {},
74
- ...error.requestId ? { requestId: error.requestId } : {}
75
- }, 429);
76
- if (error.requestId) response.headers.set("X-Request-ID", error.requestId);
77
- return response;
78
- }
79
- if (error instanceof RealtimeAvatarHttpError && error.isBilling) {
80
- return json({ code: error.code ?? "insufficient_credits" }, 402);
81
- }
82
- throw error;
83
- }
84
- };
85
- }
86
-
87
- export { createProxyHandler };