@to11ai/sdk 0.0.2

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/src/client.ts ADDED
@@ -0,0 +1,120 @@
1
+ import type { CacheConfig } from "./prompts/cache";
2
+ import { PromptResource } from "./prompts/resource";
3
+ import { Transport } from "./transport";
4
+
5
+ // Re-export all error types for convenience
6
+ export {
7
+ To11aiApiError,
8
+ To11aiAuthError,
9
+ To11aiError,
10
+ To11aiNetworkError,
11
+ To11aiRateLimitError,
12
+ To11aiValidationError,
13
+ } from "./errors";
14
+ export type { GatewayAuthOptions } from "./gateway";
15
+ export { gatewayAuthHeaders, gatewayPromptHeaders } from "./gateway";
16
+ export type { CacheConfig } from "./prompts/cache";
17
+ export { PromptResource } from "./prompts/resource";
18
+ export type { Transport } from "./transport";
19
+
20
+ // ─── Auth option shapes ────────────────────────────────────────────────────
21
+
22
+ interface ServerAuthOptions {
23
+ /** API key for server-side use. Must not be used in browser contexts. */
24
+ apiKey: string;
25
+ getAccessToken?: never;
26
+ }
27
+
28
+ interface BrowserAuthOptions {
29
+ /** Token callback for browser / edge contexts — avoids static secrets. */
30
+ getAccessToken: () => Promise<string>;
31
+ apiKey?: never;
32
+ }
33
+
34
+ type AuthOptions = ServerAuthOptions | BrowserAuthOptions;
35
+
36
+ // ─── Client options ────────────────────────────────────────────────────────
37
+
38
+ export type ClientOptions = AuthOptions & {
39
+ baseUrl: string;
40
+ /** LRU cache configuration for resolve(). Defaults: ttlSeconds=300, maxEntries=100 */
41
+ cache?: CacheConfig;
42
+ /** Request timeout in milliseconds. Default: 30_000 */
43
+ timeoutMs?: number;
44
+ /** Max retry attempts for network errors / 5xx. Default: 3 */
45
+ maxRetries?: number;
46
+ };
47
+
48
+ // ─── Client interface ──────────────────────────────────────────────────────
49
+
50
+ export interface To11aiClient {
51
+ readonly prompts: PromptResource;
52
+ }
53
+
54
+ // ─── Factory ───────────────────────────────────────────────────────────────
55
+
56
+ /**
57
+ * Create a to11ai SDK client.
58
+ *
59
+ * Auth mode is inferred from options:
60
+ * - `apiKey` → server-side; rejected in browser contexts.
61
+ * - `getAccessToken` → browser-safe; token fetched per request.
62
+ */
63
+ export function createClient(options: ClientOptions): To11aiClient {
64
+ const isDomBrowser =
65
+ typeof globalThis === "object" &&
66
+ typeof (globalThis as { document?: unknown }).document !== "undefined";
67
+ const workerLikeGlobal = globalThis as {
68
+ Bun?: {
69
+ isMainThread?: boolean;
70
+ };
71
+ importScripts?: unknown;
72
+ postMessage?: unknown;
73
+ onmessage?: unknown;
74
+ };
75
+ const isBunMainThread = workerLikeGlobal.Bun?.isMainThread === true;
76
+ const isWorkerBrowser =
77
+ typeof globalThis === "object" &&
78
+ (typeof workerLikeGlobal.importScripts === "function" ||
79
+ (!isBunMainThread &&
80
+ typeof workerLikeGlobal.postMessage === "function" &&
81
+ typeof workerLikeGlobal.onmessage !== "undefined"));
82
+
83
+ if ((isDomBrowser || isWorkerBrowser) && options.apiKey !== undefined) {
84
+ throw new Error(
85
+ "[to11ai/sdk] Do not use `apiKey` in browser contexts — use `getAccessToken` instead.",
86
+ );
87
+ }
88
+
89
+ if (options.apiKey !== undefined && options.apiKey.trim() === "") {
90
+ throw new Error("[to11ai/sdk] `apiKey` must not be an empty string.");
91
+ }
92
+
93
+ // TypeScript's union type prevents callers from providing both, but JS consumers can.
94
+ // Perform an explicit XOR check at runtime to catch the case where both are supplied.
95
+ const hasApiKey = options.apiKey !== undefined;
96
+ const hasGetAccessToken = options.getAccessToken !== undefined;
97
+ const getAccessToken = options.getAccessToken;
98
+
99
+ if (hasApiKey === hasGetAccessToken) {
100
+ throw new Error(
101
+ "[to11ai/sdk] Provide exactly one of `apiKey` (server) or `getAccessToken` (browser).",
102
+ );
103
+ }
104
+
105
+ const auth =
106
+ options.apiKey !== undefined
107
+ ? ({ apiKey: options.apiKey } as const)
108
+ : ({ getAccessToken: getAccessToken as () => Promise<string> } as const);
109
+
110
+ const transport = new Transport({
111
+ baseUrl: options.baseUrl,
112
+ auth,
113
+ timeoutMs: options.timeoutMs,
114
+ maxRetries: options.maxRetries,
115
+ });
116
+
117
+ const prompts = new PromptResource(transport, options.cache);
118
+
119
+ return { prompts };
120
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Base class for all @to11ai/sdk errors.
3
+ */
4
+ export class To11aiError extends Error {
5
+ constructor(message: string, options?: ErrorOptions) {
6
+ super(message, options);
7
+ this.name = "To11aiError";
8
+ // Maintain proper prototype chain in environments that transpile class syntax
9
+ Object.setPrototypeOf(this, new.target.prototype);
10
+ }
11
+ }
12
+
13
+ /**
14
+ * Thrown when the API returns a non-2xx response with an error envelope.
15
+ */
16
+ export class To11aiApiError extends To11aiError {
17
+ readonly statusCode: number;
18
+ readonly errorCode: string | undefined;
19
+ readonly details: unknown;
20
+
21
+ constructor(
22
+ message: string,
23
+ statusCode: number,
24
+ errorCode?: string,
25
+ details?: unknown,
26
+ ) {
27
+ super(message);
28
+ this.name = "To11aiApiError";
29
+ this.statusCode = statusCode;
30
+ this.errorCode = errorCode;
31
+ this.details = details;
32
+ Object.setPrototypeOf(this, new.target.prototype);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Thrown on 400 / schema validation failures.
38
+ */
39
+ export class To11aiValidationError extends To11aiApiError {
40
+ constructor(message: string, errorCode?: string, details?: unknown) {
41
+ super(message, 400, errorCode, details);
42
+ this.name = "To11aiValidationError";
43
+ Object.setPrototypeOf(this, new.target.prototype);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Thrown on 401 or 403 responses.
49
+ */
50
+ export class To11aiAuthError extends To11aiApiError {
51
+ constructor(
52
+ message: string,
53
+ statusCode: 401 | 403,
54
+ errorCode?: string,
55
+ details?: unknown,
56
+ ) {
57
+ super(message, statusCode, errorCode, details);
58
+ this.name = "To11aiAuthError";
59
+ Object.setPrototypeOf(this, new.target.prototype);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Thrown on 429 rate limit responses.
65
+ */
66
+ export class To11aiRateLimitError extends To11aiApiError {
67
+ readonly retryAfterSeconds: number | undefined;
68
+
69
+ constructor(
70
+ message: string,
71
+ retryAfterSeconds?: number,
72
+ errorCode?: string,
73
+ details?: unknown,
74
+ ) {
75
+ super(message, 429, errorCode, details);
76
+ this.name = "To11aiRateLimitError";
77
+ this.retryAfterSeconds = retryAfterSeconds;
78
+ Object.setPrototypeOf(this, new.target.prototype);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Thrown on network failure or request timeout.
84
+ */
85
+ export class To11aiNetworkError extends To11aiError {
86
+ constructor(message: string, cause?: unknown) {
87
+ super(message, { cause });
88
+ this.name = "To11aiNetworkError";
89
+ Object.setPrototypeOf(this, new.target.prototype);
90
+ }
91
+ }
package/src/gateway.ts ADDED
@@ -0,0 +1,49 @@
1
+ import type { ResolvedPrompt } from "./prompts/types";
2
+
3
+ export interface GatewayAuthOptions {
4
+ apiKey: string;
5
+ projectId: string;
6
+ }
7
+
8
+ /**
9
+ * Build the auth headers required for every request through the to11 gateway.
10
+ *
11
+ * @example
12
+ * const headers = gatewayAuthHeaders({ apiKey, projectId });
13
+ */
14
+ export function gatewayAuthHeaders(
15
+ options: GatewayAuthOptions,
16
+ ): Record<string, string> {
17
+ if (options.apiKey.trim().length === 0) {
18
+ throw new Error("[to11ai/sdk] `apiKey` must not be empty.");
19
+ }
20
+ if (options.projectId.trim().length === 0) {
21
+ throw new Error("[to11ai/sdk] `projectId` must not be empty.");
22
+ }
23
+ return {
24
+ "x-to11-authorization": `Bearer ${options.apiKey}`,
25
+ "x-to11-project-id": options.projectId,
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Build prompt provenance headers from a resolved prompt.
31
+ * Omits `x-to11-prompt-labels` when the prompt has no labels,
32
+ * since the gateway rejects empty header values.
33
+ *
34
+ * @example
35
+ * const prompt = await client.prompts.resolve({ slug, labels });
36
+ * const headers = gatewayPromptHeaders(prompt);
37
+ */
38
+ export function gatewayPromptHeaders(
39
+ prompt: ResolvedPrompt,
40
+ ): Record<string, string> {
41
+ const headers: Record<string, string> = {
42
+ "x-to11-prompt-id": prompt.promptId,
43
+ "x-to11-prompt-version": String(prompt.version),
44
+ };
45
+ if (prompt.labels.length > 0) {
46
+ headers["x-to11-prompt-labels"] = prompt.labels.join(",");
47
+ }
48
+ return headers;
49
+ }
@@ -0,0 +1,122 @@
1
+ import type { ResolvedPrompt } from "./types";
2
+
3
+ export interface CacheConfig {
4
+ /** Time-to-live in seconds. Default: 300 (5 minutes) */
5
+ ttlSeconds?: number;
6
+ /** Maximum number of entries. Default: 100 */
7
+ maxEntries?: number;
8
+ }
9
+
10
+ interface CacheEntry {
11
+ value: ResolvedPrompt;
12
+ expiresAt: number;
13
+ }
14
+
15
+ /**
16
+ * Build a canonical cache key for a resolve() call.
17
+ *
18
+ * - projectId is included to prevent cross-project cache collisions: a single
19
+ * PromptResource can serve multiple projects, so the same slug+labels must
20
+ * not share a cache entry across different projects.
21
+ * - Labels are sorted alphabetically so ["production", "en"] and ["en", "production"]
22
+ * map to the same key.
23
+ * - Variables are canonicalized with deterministic sorted-keys JSON before hashing.
24
+ * - Each segment is JSON-encoded so that separator characters in slug or label
25
+ * values cannot cause key collisions (e.g. a slug containing "|" or ",").
26
+ */
27
+ export function buildCacheKey(
28
+ projectId: string,
29
+ slug: string,
30
+ labels: string[],
31
+ variables?: Record<string, unknown>,
32
+ ): string {
33
+ const sortedLabels = [...labels].sort();
34
+ const varsPart =
35
+ variables !== undefined ? JSON.stringify(sortObjectKeys(variables)) : null;
36
+ // JSON.stringify each segment to prevent separator-character collisions
37
+ return `${JSON.stringify(projectId)}|${JSON.stringify(slug)}|${JSON.stringify(sortedLabels)}|${JSON.stringify(varsPart)}`;
38
+ }
39
+
40
+ function sortObjectKeys(obj: unknown): unknown {
41
+ if (obj === null || typeof obj !== "object") return obj;
42
+ if (Array.isArray(obj)) return obj.map(sortObjectKeys);
43
+ const sorted: Record<string, unknown> = {};
44
+ for (const key of Object.keys(obj as Record<string, unknown>).sort()) {
45
+ sorted[key] = sortObjectKeys((obj as Record<string, unknown>)[key]);
46
+ }
47
+ return sorted;
48
+ }
49
+
50
+ /**
51
+ * In-memory LRU cache for ResolvedPrompt objects.
52
+ *
53
+ * Uses a Map (insertion-ordered) as the backing store. On every access, the
54
+ * entry is deleted and re-inserted to maintain LRU order — oldest entries sit
55
+ * at the front of the Map.
56
+ */
57
+ export class PromptCache {
58
+ private readonly ttlMs: number;
59
+ private readonly maxEntries: number;
60
+ private readonly store: Map<string, CacheEntry>;
61
+
62
+ constructor(config?: CacheConfig) {
63
+ this.ttlMs = (config?.ttlSeconds ?? 300) * 1000;
64
+ this.maxEntries = config?.maxEntries ?? 100;
65
+ this.store = new Map();
66
+ }
67
+
68
+ get(key: string): ResolvedPrompt | undefined {
69
+ const entry = this.store.get(key);
70
+ if (!entry) return undefined;
71
+
72
+ if (Date.now() > entry.expiresAt) {
73
+ this.store.delete(key);
74
+ return undefined;
75
+ }
76
+
77
+ // Refresh LRU order — move to end
78
+ this.store.delete(key);
79
+ this.store.set(key, entry);
80
+
81
+ return entry.value;
82
+ }
83
+
84
+ set(key: string, value: ResolvedPrompt): void {
85
+ // maxEntries: 0 (or negative) means caching is disabled — drop the entry.
86
+ if (this.maxEntries <= 0) {
87
+ return;
88
+ }
89
+
90
+ // Remove existing entry so re-insertion moves it to the end
91
+ if (this.store.has(key)) {
92
+ this.store.delete(key);
93
+ }
94
+
95
+ // Evict least-recently-used entry if at capacity
96
+ while (this.store.size >= this.maxEntries) {
97
+ const lruKey = this.store.keys().next().value;
98
+ if (lruKey !== undefined) {
99
+ this.store.delete(lruKey);
100
+ } else {
101
+ break;
102
+ }
103
+ }
104
+
105
+ this.store.set(key, {
106
+ value,
107
+ expiresAt: Date.now() + this.ttlMs,
108
+ });
109
+ }
110
+
111
+ delete(key: string): void {
112
+ this.store.delete(key);
113
+ }
114
+
115
+ clear(): void {
116
+ this.store.clear();
117
+ }
118
+
119
+ get size(): number {
120
+ return this.store.size;
121
+ }
122
+ }
@@ -0,0 +1,279 @@
1
+ import { To11aiNetworkError } from "../errors";
2
+ import type { ApiEnvelope, Transport } from "../transport";
3
+ import type { CacheConfig } from "./cache";
4
+ import { buildCacheKey, PromptCache } from "./cache";
5
+ import type {
6
+ ArchivePromptInput,
7
+ ArchivePromptResult,
8
+ CreatePromptInput,
9
+ CreatePromptResult,
10
+ CreatePromptVersionInput,
11
+ CreatePromptVersionResult,
12
+ GetPromptInput,
13
+ GetPromptResult,
14
+ GetPromptVersionInput,
15
+ GetPromptVersionResult,
16
+ ListPromptLabelsInput,
17
+ ListPromptLabelsResult,
18
+ ListPromptsInput,
19
+ ListPromptsResult,
20
+ ListPromptVersionsInput,
21
+ ListPromptVersionsResult,
22
+ MovePromptLabelInput,
23
+ MovePromptLabelResult,
24
+ PaginatedResult,
25
+ ResolvePromptInput,
26
+ ResolvePromptResult,
27
+ UpdatePromptInput,
28
+ UpdatePromptResult,
29
+ } from "./types";
30
+
31
+ // Re-export CacheConfig so callers can import from this module
32
+ export type { CacheConfig } from "./cache";
33
+ export type {
34
+ ArchivePromptInput,
35
+ ArchivePromptResult,
36
+ ChatMessage,
37
+ CreatePromptInput,
38
+ CreatePromptResult,
39
+ CreatePromptVersionInput,
40
+ CreatePromptVersionResult,
41
+ GetPromptInput,
42
+ GetPromptResult,
43
+ GetPromptVersionInput,
44
+ GetPromptVersionResult,
45
+ ListPromptLabelsInput,
46
+ ListPromptLabelsResult,
47
+ ListPromptsInput,
48
+ ListPromptsResult,
49
+ ListPromptVersionsInput,
50
+ ListPromptVersionsResult,
51
+ MovePromptLabelInput,
52
+ MovePromptLabelResult,
53
+ PaginatedResult,
54
+ PaginationInfo,
55
+ Prompt,
56
+ PromptFormat,
57
+ PromptLabelPointer,
58
+ PromptStatus,
59
+ PromptVersion,
60
+ RenderedPrompt,
61
+ ResolvedPrompt,
62
+ ResolvePromptInput,
63
+ ResolvePromptResult,
64
+ UpdatePromptInput,
65
+ UpdatePromptResult,
66
+ } from "./types";
67
+
68
+ function unwrap<T>(envelope: ApiEnvelope<T>): T {
69
+ if (envelope.data === null || envelope.data === undefined) {
70
+ throw new Error("API returned success but data is null");
71
+ }
72
+ return envelope.data;
73
+ }
74
+
75
+ function getProjectPromptsPath(projectId: string): string {
76
+ return `/v1/projects/${encodeURIComponent(projectId)}/prompts`;
77
+ }
78
+
79
+ function getProjectPromptPath(projectId: string, promptId: string): string {
80
+ return `${getProjectPromptsPath(projectId)}/${encodeURIComponent(promptId)}`;
81
+ }
82
+
83
+ export class PromptResource {
84
+ private readonly transport: Transport;
85
+ private readonly cache: PromptCache;
86
+
87
+ constructor(transport: Transport, cacheConfig?: CacheConfig) {
88
+ this.transport = transport;
89
+ this.cache = new PromptCache(cacheConfig);
90
+ }
91
+
92
+ // ─── Create ──────────────────────────────────────────────────────────────
93
+
94
+ async create(input: CreatePromptInput): Promise<CreatePromptResult> {
95
+ const { projectId, ...body } = input;
96
+ const envelope = await this.transport.request<CreatePromptResult>(
97
+ "POST",
98
+ getProjectPromptsPath(projectId),
99
+ { body },
100
+ );
101
+ return unwrap(envelope);
102
+ }
103
+
104
+ // ─── List ─────────────────────────────────────────────────────────────────
105
+
106
+ async list(
107
+ input: ListPromptsInput,
108
+ ): Promise<PaginatedResult<ListPromptsResult>> {
109
+ const { projectId, ...queryRest } = input;
110
+ const envelope = await this.transport.request<
111
+ | ListPromptsResult[]
112
+ | {
113
+ items?: ListPromptsResult[];
114
+ pagination?: PaginatedResult<ListPromptsResult>["pagination"];
115
+ }
116
+ >("GET", getProjectPromptsPath(projectId), {
117
+ query: {
118
+ ...(queryRest as Record<string, string | number | boolean | undefined>),
119
+ },
120
+ });
121
+
122
+ const raw = unwrap(envelope);
123
+ // The server may return a top-level array or a { items, pagination } shape
124
+ const items = Array.isArray(raw)
125
+ ? (raw as ListPromptsResult[])
126
+ : (raw.items ?? []);
127
+ const pagination = envelope.pagination ??
128
+ (Array.isArray(raw) ? undefined : raw.pagination) ?? {
129
+ page: 1,
130
+ limit: items.length,
131
+ total: items.length,
132
+ totalPages: 1,
133
+ };
134
+
135
+ return { items, pagination };
136
+ }
137
+
138
+ // ─── Get ──────────────────────────────────────────────────────────────────
139
+
140
+ async get(input: GetPromptInput): Promise<GetPromptResult> {
141
+ const { projectId, promptId } = input;
142
+ const envelope = await this.transport.request<GetPromptResult>(
143
+ "GET",
144
+ getProjectPromptPath(projectId, promptId),
145
+ );
146
+ return unwrap(envelope);
147
+ }
148
+
149
+ // ─── Update ───────────────────────────────────────────────────────────────
150
+
151
+ async update(input: UpdatePromptInput): Promise<UpdatePromptResult> {
152
+ const { projectId, promptId, ...body } = input;
153
+ const envelope = await this.transport.request<UpdatePromptResult>(
154
+ "PATCH",
155
+ getProjectPromptPath(projectId, promptId),
156
+ { body },
157
+ );
158
+ return unwrap(envelope);
159
+ }
160
+
161
+ // ─── Archive ──────────────────────────────────────────────────────────────
162
+
163
+ async archive(input: ArchivePromptInput): Promise<ArchivePromptResult> {
164
+ const { projectId, promptId } = input;
165
+ const envelope = await this.transport.request<ArchivePromptResult>(
166
+ "DELETE",
167
+ getProjectPromptPath(projectId, promptId),
168
+ );
169
+ return unwrap(envelope);
170
+ }
171
+
172
+ // ─── Create Version ───────────────────────────────────────────────────────
173
+
174
+ async createVersion(
175
+ input: CreatePromptVersionInput,
176
+ ): Promise<CreatePromptVersionResult> {
177
+ const { projectId, promptId, ...body } = input;
178
+ const envelope = await this.transport.request<CreatePromptVersionResult>(
179
+ "POST",
180
+ `${getProjectPromptPath(projectId, promptId)}/versions`,
181
+ { body },
182
+ );
183
+ return unwrap(envelope);
184
+ }
185
+
186
+ // ─── List Versions ────────────────────────────────────────────────────────
187
+
188
+ async listVersions(
189
+ input: ListPromptVersionsInput,
190
+ ): Promise<ListPromptVersionsResult> {
191
+ const { projectId, promptId } = input;
192
+ const envelope = await this.transport.request<ListPromptVersionsResult>(
193
+ "GET",
194
+ `${getProjectPromptPath(projectId, promptId)}/versions`,
195
+ );
196
+ return unwrap(envelope);
197
+ }
198
+
199
+ // ─── Get Version ──────────────────────────────────────────────────────────
200
+
201
+ async getVersion(
202
+ input: GetPromptVersionInput,
203
+ ): Promise<GetPromptVersionResult> {
204
+ const { projectId, promptId, versionNumber } = input;
205
+ const envelope = await this.transport.request<GetPromptVersionResult>(
206
+ "GET",
207
+ `${getProjectPromptPath(projectId, promptId)}/versions/${encodeURIComponent(String(versionNumber))}`,
208
+ );
209
+ return unwrap(envelope);
210
+ }
211
+
212
+ // ─── Move Label ───────────────────────────────────────────────────────────
213
+
214
+ async moveLabel(input: MovePromptLabelInput): Promise<MovePromptLabelResult> {
215
+ const { projectId, promptId, label, ...body } = input;
216
+ const envelope = await this.transport.request<MovePromptLabelResult>(
217
+ "PUT",
218
+ `${getProjectPromptPath(projectId, promptId)}/labels/${encodeURIComponent(label)}`,
219
+ { body },
220
+ );
221
+ return unwrap(envelope);
222
+ }
223
+
224
+ // ─── List Labels ──────────────────────────────────────────────────────────
225
+
226
+ async listLabels(
227
+ input: ListPromptLabelsInput,
228
+ ): Promise<ListPromptLabelsResult> {
229
+ const { projectId, promptId } = input;
230
+ const envelope = await this.transport.request<ListPromptLabelsResult>(
231
+ "GET",
232
+ `${getProjectPromptPath(projectId, promptId)}/labels`,
233
+ );
234
+ return unwrap(envelope);
235
+ }
236
+
237
+ // ─── Resolve ──────────────────────────────────────────────────────────────
238
+
239
+ async resolve(input: ResolvePromptInput): Promise<ResolvePromptResult> {
240
+ const {
241
+ projectId,
242
+ slug,
243
+ labels,
244
+ variables,
245
+ cache: useCache = true,
246
+ fallback,
247
+ } = input;
248
+
249
+ const cacheKey = buildCacheKey(projectId, slug, labels, variables);
250
+
251
+ if (useCache) {
252
+ const cached = this.cache.get(cacheKey);
253
+ if (cached) return structuredClone(cached);
254
+ }
255
+
256
+ try {
257
+ const envelope = await this.transport.request<ResolvePromptResult>(
258
+ "POST",
259
+ `${getProjectPromptsPath(projectId)}:resolve`,
260
+ {
261
+ body: { slug, labels, variables },
262
+ },
263
+ );
264
+ const result = unwrap(envelope);
265
+
266
+ if (useCache) {
267
+ this.cache.set(cacheKey, structuredClone(result));
268
+ }
269
+
270
+ return result;
271
+ } catch (err) {
272
+ // If network error and we have a fallback, serve it
273
+ if (err instanceof To11aiNetworkError && fallback !== undefined) {
274
+ return fallback;
275
+ }
276
+ throw err;
277
+ }
278
+ }
279
+ }