addisai 0.1.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.cjs ADDED
@@ -0,0 +1,1465 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ // src/core/env.ts
6
+ var DEFAULT_BASE_URL = "https://api.addisassistant.com";
7
+ var API_KEY_ENV_VAR = "ADDIS_API_KEY";
8
+ function readEnv(name) {
9
+ if (typeof process !== "undefined" && process.env && name in process.env) {
10
+ const value = process.env[name];
11
+ return value == null || value === "" ? void 0 : value;
12
+ }
13
+ const deno = globalThis.Deno;
14
+ if (deno?.env?.get) {
15
+ try {
16
+ const value = deno.env.get(name);
17
+ return value ? value : void 0;
18
+ } catch {
19
+ return void 0;
20
+ }
21
+ }
22
+ return void 0;
23
+ }
24
+ function isBrowserLike() {
25
+ return typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
26
+ }
27
+ var BLOCKED_HOST_SUFFIXES = [".supabase.co", ".supabase.in"];
28
+ function resolveBaseURL(baseURL) {
29
+ const raw = (baseURL ?? DEFAULT_BASE_URL).trim().replace(/\/+$/, "");
30
+ let url;
31
+ try {
32
+ url = new URL(raw);
33
+ } catch {
34
+ throw new Error(`addisai: invalid baseURL "${raw}".`);
35
+ }
36
+ const host = url.hostname.toLowerCase();
37
+ const isLocal = host === "localhost" || host === "127.0.0.1";
38
+ if (url.protocol !== "https:" && !isLocal) {
39
+ throw new Error(
40
+ `addisai: baseURL must use https (got "${url.protocol}//${host}").`
41
+ );
42
+ }
43
+ if (BLOCKED_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix))) {
44
+ throw new Error(
45
+ `addisai: pointing the SDK at a raw *.supabase.co host is not allowed. Use the Addis AI API at ${DEFAULT_BASE_URL}.`
46
+ );
47
+ }
48
+ return raw;
49
+ }
50
+ function detectRuntime() {
51
+ const g = globalThis;
52
+ if (g.Deno?.version?.deno) return `Deno/${g.Deno.version.deno}`;
53
+ if (g.Bun?.version) return `Bun/${g.Bun.version}`;
54
+ if (typeof process !== "undefined" && process.versions?.node) {
55
+ return `Node/${process.versions.node}`;
56
+ }
57
+ if (isBrowserLike()) return "Browser";
58
+ return "Unknown";
59
+ }
60
+
61
+ // src/core/errors.ts
62
+ var AddisAIError = class extends Error {
63
+ constructor(message) {
64
+ super(message);
65
+ this.name = new.target.name;
66
+ Object.setPrototypeOf(this, new.target.prototype);
67
+ }
68
+ };
69
+ var NotSupportedError = class extends AddisAIError {
70
+ };
71
+ var APIConnectionError = class extends AddisAIError {
72
+ constructor(message = "Connection error.", cause) {
73
+ super(message);
74
+ this.cause = cause;
75
+ }
76
+ };
77
+ var APIConnectionTimeoutError = class extends APIConnectionError {
78
+ constructor(message = "Request timed out.") {
79
+ super(message);
80
+ }
81
+ };
82
+ var APIError = class extends AddisAIError {
83
+ constructor(status, message, opts = {}) {
84
+ super(message);
85
+ this.status = status;
86
+ this.code = opts.code ?? null;
87
+ this.details = opts.details ?? [];
88
+ this.requestId = opts.requestId ?? null;
89
+ this.headers = opts.headers ?? {};
90
+ }
91
+ };
92
+ var BadRequestError = class extends APIError {
93
+ };
94
+ var AuthenticationError = class extends APIError {
95
+ };
96
+ var InsufficientCreditsError = class extends APIError {
97
+ // 402
98
+ get availableBalance() {
99
+ return numericDetail(this, "balance_too_low") ?? null;
100
+ }
101
+ };
102
+ var PermissionDeniedError = class extends APIError {
103
+ };
104
+ var NotFoundError = class extends APIError {
105
+ };
106
+ var ConflictError = class extends APIError {
107
+ };
108
+ var IdempotencyConflictError = class extends ConflictError {
109
+ };
110
+ var GenerationInProgressError = class extends ConflictError {
111
+ // 409 GENERATION_IN_PROGRESS
112
+ get retryAfter() {
113
+ return headerNumber(this.headers, "retry-after");
114
+ }
115
+ };
116
+ var UnprocessableEntityError = class extends APIError {
117
+ };
118
+ var RateLimitError = class extends APIError {
119
+ // 429
120
+ get retryAfter() {
121
+ return headerNumber(this.headers, "retry-after");
122
+ }
123
+ get limit() {
124
+ return headerNumber(this.headers, "x-ratelimit-limit");
125
+ }
126
+ get remaining() {
127
+ return headerNumber(this.headers, "x-ratelimit-remaining");
128
+ }
129
+ get reset() {
130
+ return headerNumber(this.headers, "x-ratelimit-reset");
131
+ }
132
+ };
133
+ var InternalServerError = class extends APIError {
134
+ };
135
+ function headerNumber(headers, name) {
136
+ const raw = headers[name.toLowerCase()];
137
+ if (!raw) return null;
138
+ const n = Number(raw);
139
+ return Number.isFinite(n) ? n : null;
140
+ }
141
+ function numericDetail(err, code) {
142
+ const detail = err.details.find((d) => d.code === code);
143
+ const match = detail?.message?.match(/([\d]+(?:\.\d+)?)/g);
144
+ if (!match) return null;
145
+ const n = Number(match[match.length - 1]);
146
+ return Number.isFinite(n) ? n : null;
147
+ }
148
+ function parseEnvelope(status, body, rawText) {
149
+ if (body && typeof body === "object") {
150
+ const obj = body;
151
+ const err = obj.error;
152
+ if (err && typeof err === "object") {
153
+ return {
154
+ message: typeof err.message === "string" && err.message ? err.message : `Request failed with status ${status}.`,
155
+ code: typeof err.code === "string" ? err.code : typeof err.type === "string" ? err.type : null,
156
+ details: Array.isArray(err.details) ? err.details : []
157
+ };
158
+ }
159
+ if (typeof obj.message === "string" && obj.message) {
160
+ return { message: obj.message, code: typeof obj.code === "string" ? obj.code : null, details: [] };
161
+ }
162
+ }
163
+ return {
164
+ message: rawText?.slice(0, 500) || `Request failed with status ${status}.`,
165
+ code: null,
166
+ details: []
167
+ };
168
+ }
169
+ function makeAPIError(status, body, rawText, headers) {
170
+ const { message, code, details } = parseEnvelope(status, body, rawText);
171
+ const requestId = headers["x-request-id"] ?? headers["cf-ray"] ?? null;
172
+ const opts = { code, details, requestId, headers };
173
+ const upper = (code ?? "").toUpperCase();
174
+ switch (status) {
175
+ case 400:
176
+ return new BadRequestError(status, message, opts);
177
+ case 401:
178
+ return new AuthenticationError(status, message, opts);
179
+ case 402:
180
+ return new InsufficientCreditsError(status, message, opts);
181
+ case 403:
182
+ return new PermissionDeniedError(status, message, opts);
183
+ case 404:
184
+ return new NotFoundError(status, message, opts);
185
+ case 409:
186
+ if (upper === "IDEMPOTENCY_CONFLICT") return new IdempotencyConflictError(status, message, opts);
187
+ if (upper === "GENERATION_IN_PROGRESS") return new GenerationInProgressError(status, message, opts);
188
+ return new ConflictError(status, message, opts);
189
+ case 413:
190
+ case 422:
191
+ return new UnprocessableEntityError(status, message, opts);
192
+ case 429:
193
+ return new RateLimitError(status, message, opts);
194
+ default:
195
+ if (status >= 500) return new InternalServerError(status, message, opts);
196
+ return new APIError(status, message, opts);
197
+ }
198
+ }
199
+
200
+ // src/version.ts
201
+ var VERSION = "0.1.0";
202
+
203
+ // src/core/request.ts
204
+ var LOG_ORDER = {
205
+ off: 0,
206
+ error: 1,
207
+ warn: 2,
208
+ info: 3,
209
+ debug: 4
210
+ };
211
+ var Transport = class {
212
+ constructor(config) {
213
+ this.config = config;
214
+ }
215
+ /** The configured fetch implementation, for direct asset downloads. */
216
+ get fetch() {
217
+ return this.config.fetch;
218
+ }
219
+ log(level, ...args) {
220
+ if (LOG_ORDER[this.config.logLevel] >= LOG_ORDER[level]) {
221
+ console[level === "debug" ? "log" : level]("[addisai]", ...args);
222
+ }
223
+ }
224
+ buildURL(path, query) {
225
+ const url = new URL(this.config.baseURL + path);
226
+ const merged = { ...this.config.defaultQuery, ...query ?? {} };
227
+ for (const [key, value] of Object.entries(merged)) {
228
+ if (value == null) continue;
229
+ url.searchParams.set(key, String(value));
230
+ }
231
+ return url.toString();
232
+ }
233
+ buildHeaders(req, opts) {
234
+ const headers = new Headers();
235
+ headers.set("Accept", "application/json");
236
+ headers.set("User-Agent", `addisai-node/${VERSION} (${detectRuntime()})`);
237
+ headers.set("X-Addis-Client", `addisai-node/${VERSION}`);
238
+ if (looksLikeJwt(this.config.apiKey)) {
239
+ headers.set("Authorization", `Bearer ${this.config.apiKey}`);
240
+ } else {
241
+ headers.set("x-api-key", this.config.apiKey);
242
+ }
243
+ for (const [k, v] of Object.entries(this.config.defaultHeaders)) headers.set(k, v);
244
+ for (const [k, v] of Object.entries(opts.headers ?? {})) headers.set(k, v);
245
+ if (opts.idempotencyKey) headers.set("Idempotency-Key", opts.idempotencyKey);
246
+ if (req.body !== void 0 && !req.form) headers.set("Content-Type", "application/json");
247
+ return headers;
248
+ }
249
+ isRetryable(status, headers) {
250
+ if (status === 408 || status === 425 || status === 429) return true;
251
+ if (status === 409) return headers.has("retry-after");
252
+ return status >= 500;
253
+ }
254
+ retryDelayMs(attempt, headers) {
255
+ const retryAfter = headers.get("retry-after");
256
+ if (retryAfter) {
257
+ const seconds = Number(retryAfter);
258
+ if (Number.isFinite(seconds)) return Math.min(seconds * 1e3, 6e4);
259
+ }
260
+ const base = Math.min(500 * 2 ** attempt, 8e3);
261
+ return base * (0.5 + Math.random() * 0.5);
262
+ }
263
+ async request(req, opts = {}) {
264
+ const maxRetries = opts.maxRetries ?? this.config.maxRetries;
265
+ let timeout = opts.timeout ?? this.config.timeout;
266
+ if (req.timeoutFloor && timeout < req.timeoutFloor && opts.timeout === void 0) {
267
+ timeout = req.timeoutFloor;
268
+ }
269
+ const url = this.buildURL(req.path, { ...req.query, ...opts.query });
270
+ const headers = this.buildHeaders(req, opts);
271
+ const bodyInit = req.form ? req.form : req.body !== void 0 ? JSON.stringify(req.body) : void 0;
272
+ let lastError;
273
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
274
+ const controller = new AbortController();
275
+ const onAbort = () => controller.abort(opts.signal?.reason);
276
+ if (opts.signal) {
277
+ if (opts.signal.aborted) controller.abort(opts.signal.reason);
278
+ else opts.signal.addEventListener("abort", onAbort, { once: true });
279
+ }
280
+ const timer = setTimeout(() => controller.abort(new DOMExceptionLike("timeout")), timeout);
281
+ let response;
282
+ try {
283
+ this.log("debug", `${req.method} ${url} (attempt ${attempt + 1}/${maxRetries + 1})`);
284
+ response = await this.config.fetch(url, {
285
+ method: req.method,
286
+ headers,
287
+ body: bodyInit,
288
+ signal: controller.signal
289
+ });
290
+ } catch (err) {
291
+ clearTimeout(timer);
292
+ opts.signal?.removeEventListener("abort", onAbort);
293
+ if (opts.signal?.aborted) throw err;
294
+ const timedOut = isAbortError(err);
295
+ lastError = timedOut ? new APIConnectionTimeoutError(`Request timed out after ${timeout}ms.`) : new APIConnectionError("Connection error.", err);
296
+ if (attempt < maxRetries) {
297
+ await sleep(this.retryDelayMs(attempt, new Headers()));
298
+ continue;
299
+ }
300
+ throw lastError;
301
+ } finally {
302
+ clearTimeout(timer);
303
+ opts.signal?.removeEventListener("abort", onAbort);
304
+ }
305
+ if (response.ok) {
306
+ if (req.raw) return response;
307
+ return await this.parseJSON(response);
308
+ }
309
+ if (attempt < maxRetries && this.isRetryable(response.status, response.headers)) {
310
+ const delay = this.retryDelayMs(attempt, response.headers);
311
+ this.log("warn", `Retrying after ${Math.round(delay)}ms (status ${response.status}).`);
312
+ await sleep(delay);
313
+ continue;
314
+ }
315
+ throw await this.toError(response);
316
+ }
317
+ throw lastError instanceof Error ? lastError : new APIConnectionError();
318
+ }
319
+ /**
320
+ * Open a streaming request: a single fetch (streams are not retried once
321
+ * started), with the timeout guarding only the initial response. The returned
322
+ * controller stays live so the caller can cancel the body mid-stream.
323
+ */
324
+ async openStream(req, opts = {}) {
325
+ const controller = new AbortController();
326
+ if (opts.signal) {
327
+ if (opts.signal.aborted) controller.abort(opts.signal.reason);
328
+ else opts.signal.addEventListener("abort", () => controller.abort(opts.signal?.reason), { once: true });
329
+ }
330
+ const timeout = opts.timeout ?? this.config.timeout;
331
+ const url = this.buildURL(req.path, { ...req.query, ...opts.query });
332
+ const headers = this.buildHeaders(req, opts);
333
+ const bodyInit = req.form ? req.form : req.body !== void 0 ? JSON.stringify(req.body) : void 0;
334
+ const timer = setTimeout(() => controller.abort(new DOMExceptionLike("timeout")), timeout);
335
+ let response;
336
+ try {
337
+ response = await this.config.fetch(url, {
338
+ method: req.method,
339
+ headers,
340
+ body: bodyInit,
341
+ signal: controller.signal
342
+ });
343
+ } catch (err) {
344
+ clearTimeout(timer);
345
+ if (opts.signal?.aborted) throw err;
346
+ throw isAbortError(err) ? new APIConnectionTimeoutError(`Request timed out after ${timeout}ms.`) : new APIConnectionError("Connection error.", err);
347
+ }
348
+ clearTimeout(timer);
349
+ if (!response.ok) throw await this.toError(response);
350
+ return { response, controller };
351
+ }
352
+ async parseJSON(response) {
353
+ const text = await response.text();
354
+ if (!text) return {};
355
+ try {
356
+ return JSON.parse(text);
357
+ } catch {
358
+ return { raw: text };
359
+ }
360
+ }
361
+ async toError(response) {
362
+ const text = await response.text().catch(() => "");
363
+ let body = void 0;
364
+ try {
365
+ body = text ? JSON.parse(text) : void 0;
366
+ } catch {
367
+ body = void 0;
368
+ }
369
+ const headers = {};
370
+ response.headers.forEach((value, key) => headers[key.toLowerCase()] = value);
371
+ return makeAPIError(response.status, body, text, headers);
372
+ }
373
+ };
374
+ var DOMExceptionLike = class extends Error {
375
+ constructor(name) {
376
+ super(name);
377
+ this.name = name;
378
+ }
379
+ };
380
+ function looksLikeJwt(key) {
381
+ return key.startsWith("ey") && key.split(".").length === 3;
382
+ }
383
+ function isAbortError(err) {
384
+ return err instanceof Error && (err.name === "AbortError" || err.name === "timeout" || err.name === "TimeoutError");
385
+ }
386
+ function sleep(ms) {
387
+ return new Promise((resolve) => setTimeout(resolve, ms));
388
+ }
389
+ function unwrapData(body) {
390
+ if (body && typeof body === "object" && "data" in body) {
391
+ return body.data;
392
+ }
393
+ return body;
394
+ }
395
+
396
+ // src/core/redact.ts
397
+ function redactApiKey(key) {
398
+ if (!key) return "(none)";
399
+ if (key.length <= 8) return "\u2022\u2022\u2022\u2022";
400
+ return `${key.slice(0, 4)}\u2022\u2022\u2022\u2022${key.slice(-2)}`;
401
+ }
402
+
403
+ // src/core/sse.ts
404
+ async function* parseSSE(body) {
405
+ const reader = body.getReader();
406
+ const decoder = new TextDecoder();
407
+ let buffer = "";
408
+ let eventName = null;
409
+ let dataLines = [];
410
+ const flush = () => {
411
+ if (dataLines.length === 0 && eventName === null) return null;
412
+ const evt = { event: eventName, data: dataLines.join("\n") };
413
+ eventName = null;
414
+ dataLines = [];
415
+ return evt;
416
+ };
417
+ try {
418
+ while (true) {
419
+ const { done, value } = await reader.read();
420
+ if (done) break;
421
+ buffer += decoder.decode(value, { stream: true });
422
+ let nl;
423
+ while ((nl = buffer.indexOf("\n")) >= 0) {
424
+ let line = buffer.slice(0, nl);
425
+ buffer = buffer.slice(nl + 1);
426
+ if (line.endsWith("\r")) line = line.slice(0, -1);
427
+ if (line === "") {
428
+ const evt2 = flush();
429
+ if (evt2) yield evt2;
430
+ continue;
431
+ }
432
+ if (line.startsWith(":")) continue;
433
+ const colon = line.indexOf(":");
434
+ const field = colon === -1 ? line : line.slice(0, colon);
435
+ let val = colon === -1 ? "" : line.slice(colon + 1);
436
+ if (val.startsWith(" ")) val = val.slice(1);
437
+ if (field === "event") eventName = val;
438
+ else if (field === "data") dataLines.push(val);
439
+ }
440
+ }
441
+ const evt = flush();
442
+ if (evt) yield evt;
443
+ } finally {
444
+ reader.releaseLock();
445
+ }
446
+ }
447
+ async function* parseNDJSON(body) {
448
+ const reader = body.getReader();
449
+ const decoder = new TextDecoder();
450
+ let buffer = "";
451
+ try {
452
+ while (true) {
453
+ const { done, value } = await reader.read();
454
+ if (done) break;
455
+ buffer += decoder.decode(value, { stream: true });
456
+ let nl;
457
+ while ((nl = buffer.indexOf("\n")) >= 0) {
458
+ const line = buffer.slice(0, nl).trim();
459
+ buffer = buffer.slice(nl + 1);
460
+ if (line) yield safeParse(line);
461
+ }
462
+ }
463
+ const tail = buffer.trim();
464
+ if (tail) yield safeParse(tail);
465
+ } finally {
466
+ reader.releaseLock();
467
+ }
468
+ }
469
+ function safeParse(line) {
470
+ try {
471
+ return JSON.parse(line);
472
+ } catch {
473
+ return void 0;
474
+ }
475
+ }
476
+
477
+ // src/lib/chat-stream.ts
478
+ var ADDIS_CHAT_MODEL = "addis-1-alef";
479
+ var ChatStream = class {
480
+ constructor(response, controller) {
481
+ this.response = response;
482
+ this.controller = controller;
483
+ this.id = `chatcmpl-${cryptoRandom()}`;
484
+ this.created = Math.floor(Date.now() / 1e3);
485
+ this.consumed = false;
486
+ }
487
+ /** Cancel the stream and underlying request. */
488
+ abort() {
489
+ this.controller?.abort();
490
+ }
491
+ async *[Symbol.asyncIterator]() {
492
+ if (this.consumed) {
493
+ throw new AddisAIError("This stream has already been consumed.");
494
+ }
495
+ this.consumed = true;
496
+ if (!this.response.body) return;
497
+ for await (const evt of parseSSE(this.response.body)) {
498
+ const raw = evt.data.trim();
499
+ if (!raw || raw === "[DONE]") {
500
+ if (raw === "[DONE]") return;
501
+ continue;
502
+ }
503
+ let payload;
504
+ try {
505
+ payload = JSON.parse(raw);
506
+ } catch {
507
+ continue;
508
+ }
509
+ if (payload.type === "metadata") {
510
+ this.transcription = {
511
+ raw: payload.transcription_raw,
512
+ clean: payload.transcription_clean
513
+ };
514
+ continue;
515
+ }
516
+ const usage = payload.usage_metadata;
517
+ if (usage) {
518
+ this.finalUsage = {
519
+ prompt_tokens: usage.prompt_token_count ?? usage.promptTokenCount ?? 0,
520
+ completion_tokens: usage.candidates_token_count ?? usage.candidatesTokenCount ?? 0,
521
+ total_tokens: usage.total_token_count ?? usage.totalTokenCount ?? 0
522
+ };
523
+ }
524
+ const content = payload.text ?? payload.response_text ?? payload.delta?.content ?? "";
525
+ const finishReason = payload.finish_reason ?? (payload.is_last_chunk ? "stop" : null);
526
+ if (!content && !finishReason) continue;
527
+ yield {
528
+ id: this.id,
529
+ object: "chat.completion.chunk",
530
+ created: this.created,
531
+ model: ADDIS_CHAT_MODEL,
532
+ choices: [{ index: 0, delta: { content }, finish_reason: normalizeFinish(finishReason) }]
533
+ };
534
+ }
535
+ }
536
+ /** Consume the stream and return the concatenated assistant text. */
537
+ async finalText() {
538
+ let text = "";
539
+ for await (const chunk of this) text += chunk.choices[0]?.delta?.content ?? "";
540
+ return text;
541
+ }
542
+ /** Consume the stream and return an assembled non-streaming completion. */
543
+ async finalCompletion() {
544
+ let content = "";
545
+ let finishReason = "stop";
546
+ for await (const chunk of this) {
547
+ content += chunk.choices[0]?.delta?.content ?? "";
548
+ if (chunk.choices[0]?.finish_reason) finishReason = chunk.choices[0].finish_reason;
549
+ }
550
+ const completion = {
551
+ id: this.id,
552
+ object: "chat.completion",
553
+ created: this.created,
554
+ model: ADDIS_CHAT_MODEL,
555
+ choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: finishReason }],
556
+ usage: this.finalUsage
557
+ };
558
+ if (this.transcription) completion.transcription = this.transcription;
559
+ return completion;
560
+ }
561
+ /** Re-encode the stream as a byte ReadableStream of SSE chunks (for piping). */
562
+ toReadableStream() {
563
+ const encoder = new TextEncoder();
564
+ const iterator = this[Symbol.asyncIterator]();
565
+ return new ReadableStream({
566
+ async pull(controller) {
567
+ const { value, done } = await iterator.next();
568
+ if (done) {
569
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
570
+ controller.close();
571
+ return;
572
+ }
573
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(value)}
574
+
575
+ `));
576
+ }
577
+ });
578
+ }
579
+ };
580
+ function normalizeFinish(reason) {
581
+ if (!reason) return null;
582
+ switch (reason.toUpperCase()) {
583
+ case "STOP":
584
+ return "stop";
585
+ case "MAX_TOKENS":
586
+ return "length";
587
+ case "SAFETY":
588
+ case "RECITATION":
589
+ return "content_filter";
590
+ case "TOOL_CALLS":
591
+ return "tool_calls";
592
+ default:
593
+ return reason.toLowerCase();
594
+ }
595
+ }
596
+ function cryptoRandom() {
597
+ const c = globalThis.crypto;
598
+ if (c?.randomUUID) return c.randomUUID();
599
+ return Math.random().toString(36).slice(2);
600
+ }
601
+
602
+ // src/core/uploads.ts
603
+ function isFileInput(value) {
604
+ return typeof value === "object" && value !== null && "data" in value;
605
+ }
606
+ function toBlob(input, fallbackType = "application/octet-stream") {
607
+ if (isFileInput(input)) {
608
+ const type = input.contentType ?? fallbackType;
609
+ const blob = input.data instanceof Blob ? input.contentType ? new Blob([input.data], { type }) : input.data : new Blob([input.data], { type });
610
+ return { blob, filename: input.filename ?? "file" };
611
+ }
612
+ if (input instanceof Blob) {
613
+ return { blob: input, filename: input.name || "file" };
614
+ }
615
+ return { blob: new Blob([input], { type: fallbackType }), filename: "file" };
616
+ }
617
+ async function fileFromPath(path, contentType) {
618
+ const fs = await import('fs/promises');
619
+ const data = await fs.readFile(path);
620
+ const filename = path.split(/[\\/]/).pop() || "file";
621
+ return {
622
+ data: new Uint8Array(data),
623
+ filename,
624
+ contentType: contentType ?? guessContentType(filename)
625
+ };
626
+ }
627
+ function guessContentType(filename) {
628
+ const ext = filename.toLowerCase().split(".").pop();
629
+ switch (ext) {
630
+ case "wav":
631
+ return "audio/wav";
632
+ case "mp3":
633
+ return "audio/mpeg";
634
+ case "m4a":
635
+ return "audio/m4a";
636
+ case "mp4":
637
+ return "audio/mp4";
638
+ case "ogg":
639
+ return "audio/ogg";
640
+ case "webm":
641
+ return "audio/webm";
642
+ case "flac":
643
+ return "audio/flac";
644
+ case "png":
645
+ return "image/png";
646
+ case "jpg":
647
+ case "jpeg":
648
+ return "image/jpeg";
649
+ case "pdf":
650
+ return "application/pdf";
651
+ default:
652
+ return void 0;
653
+ }
654
+ }
655
+
656
+ // src/resources/chat.ts
657
+ var ADDIS_CHAT_MODEL2 = "addis-1-alef";
658
+ var Chat = class {
659
+ constructor(transport) {
660
+ this.transport = transport;
661
+ this.completions = new Completions(transport);
662
+ }
663
+ /**
664
+ * Run an automatic tool-calling loop: the model requests tools, the SDK
665
+ * executes your local implementations, feeds results back, and repeats until
666
+ * the model produces a final answer.
667
+ */
668
+ async runTools(params, opts) {
669
+ const { maxToolRoundtrips = 5, tools, ...rest } = params;
670
+ const impls = /* @__PURE__ */ new Map();
671
+ const wireTools = tools.map((t) => {
672
+ impls.set(t.function.name, t.function.function);
673
+ return { type: "function", function: { name: t.function.name, description: t.function.description, parameters: t.function.parameters } };
674
+ });
675
+ const messages = [...rest.messages];
676
+ for (let i = 0; i <= maxToolRoundtrips; i++) {
677
+ const completion = await this.completions.create(
678
+ { ...rest, messages, tools: wireTools, stream: false },
679
+ opts
680
+ );
681
+ const choice = completion.choices[0];
682
+ const calls = choice?.message.tool_calls ?? [];
683
+ if (!calls.length || choice?.finish_reason !== "tool_calls") {
684
+ return completion;
685
+ }
686
+ if (i === maxToolRoundtrips) {
687
+ throw new AddisAIError(
688
+ `runTools exceeded maxToolRoundtrips (${maxToolRoundtrips}) without a final answer.`
689
+ );
690
+ }
691
+ messages.push(choice.message);
692
+ for (const call of calls) {
693
+ const impl = impls.get(call.function.name);
694
+ if (!impl) {
695
+ throw new AddisAIError(`No implementation provided for tool "${call.function.name}".`);
696
+ }
697
+ let args = {};
698
+ try {
699
+ args = call.function.arguments ? JSON.parse(call.function.arguments) : {};
700
+ } catch {
701
+ args = call.function.arguments;
702
+ }
703
+ const result = await impl(args);
704
+ messages.push({
705
+ role: "tool",
706
+ tool_call_id: call.id,
707
+ name: call.function.name,
708
+ content: typeof result === "string" ? result : JSON.stringify(result)
709
+ });
710
+ }
711
+ }
712
+ throw new AddisAIError("runTools terminated unexpectedly.");
713
+ }
714
+ };
715
+ var Completions = class {
716
+ constructor(transport) {
717
+ this.transport = transport;
718
+ }
719
+ async create(params, opts = {}) {
720
+ const hasTools = Boolean(params.tools?.length) || params.messages.some((m) => m.role === "tool" || m.tool_calls);
721
+ const language = params.language ?? "am";
722
+ if (params.stream) {
723
+ if (hasTools) {
724
+ throw new AddisAIError("Streaming is not supported with tool calling. Use non-streaming mode for tools.");
725
+ }
726
+ return this.createStream(params, language, opts);
727
+ }
728
+ if (params.attachments?.length || params.audio) {
729
+ return this.createMultipart(params, language, opts);
730
+ }
731
+ const envelope = await this.transport.request(
732
+ { method: "POST", path: "/api/v1/chat_generate", body: buildNativeBody(params, language) },
733
+ opts
734
+ );
735
+ return nativeToOpenAICompletion(envelope.data);
736
+ }
737
+ async createStream(params, language, opts) {
738
+ const { response, controller } = await this.transport.openStream(
739
+ { method: "POST", path: "/api/v1/chat_generate", body: buildNativeBody(params, language, { stream: true }) },
740
+ opts
741
+ );
742
+ return new ChatStream(response, controller);
743
+ }
744
+ async createMultipart(params, language, opts) {
745
+ const form = new FormData();
746
+ const attachmentFieldNames = [];
747
+ (params.attachments ?? []).forEach((att, i) => {
748
+ const field = att.name ?? `attachment_${i}`;
749
+ const { blob, filename } = toBlob(att.file);
750
+ form.append(field, blob, filename);
751
+ attachmentFieldNames.push(field);
752
+ });
753
+ if (params.audio) {
754
+ const { blob, filename } = toBlob(params.audio, "audio/wav");
755
+ form.append("chat_audio_input", blob, filename || "audio.wav");
756
+ }
757
+ const requestData = buildNativeBody(params, language);
758
+ if (attachmentFieldNames.length) requestData.attachment_field_names = attachmentFieldNames;
759
+ form.append(
760
+ "request_data",
761
+ new Blob([JSON.stringify(requestData)], { type: "application/json" })
762
+ );
763
+ const envelope = await this.transport.request(
764
+ { method: "POST", path: "/api/v1/chat_generate", form },
765
+ opts
766
+ );
767
+ return nativeToOpenAICompletion(envelope.data);
768
+ }
769
+ };
770
+ function buildNativeBody(params, language, opts = {}) {
771
+ const systemParts = [];
772
+ if (params.system) systemParts.push(params.system);
773
+ const nonSystem = [];
774
+ for (const m of params.messages) {
775
+ if (m.role === "system" || m.role === "developer") {
776
+ if (typeof m.content === "string" && m.content.trim()) systemParts.push(m.content);
777
+ } else {
778
+ nonSystem.push(m);
779
+ }
780
+ }
781
+ let lastUser = -1;
782
+ for (let i = nonSystem.length - 1; i >= 0; i--) {
783
+ const m = nonSystem[i];
784
+ if (m.role === "user" && typeof m.content === "string" && m.content.trim()) {
785
+ lastUser = i;
786
+ break;
787
+ }
788
+ }
789
+ const prompt = lastUser >= 0 ? nonSystem[lastUser].content : void 0;
790
+ const historySource = lastUser >= 0 ? nonSystem.slice(0, lastUser) : nonSystem;
791
+ const conversationHistory = historySource.map((m) => ({
792
+ role: m.role === "assistant" ? "assistant" : m.role === "tool" ? "assistant" : "user",
793
+ content: typeof m.content === "string" ? m.content : ""
794
+ })).filter((m) => m.content);
795
+ const body = { target_language: language };
796
+ if (prompt !== void 0) body.prompt = prompt;
797
+ if (conversationHistory.length) body.conversation_history = conversationHistory;
798
+ if (systemParts.length) body.system = systemParts.join("\n\n");
799
+ if (params.persona) body.persona = params.persona;
800
+ if (params.tools) body.tools = params.tools;
801
+ if (params.tool_choice !== void 0) body.tool_choice = params.tool_choice;
802
+ const gen = {};
803
+ if (params.temperature !== void 0) gen.temperature = params.temperature;
804
+ if (params.max_tokens !== void 0) gen.maxOutputTokens = params.max_tokens;
805
+ if (opts.stream) gen.stream = true;
806
+ if (Object.keys(gen).length) body.generation_config = gen;
807
+ return body;
808
+ }
809
+ function nativeToOpenAICompletion(data) {
810
+ const toolCalls = data.tool_calls?.length ? data.tool_calls : void 0;
811
+ const message = toolCalls ? { role: "assistant", content: data.response_text || null, tool_calls: toolCalls } : { role: "assistant", content: data.response_text ?? "" };
812
+ const completion = {
813
+ id: `chatcmpl-${cryptoRandom2()}`,
814
+ object: "chat.completion",
815
+ created: Math.floor(Date.now() / 1e3),
816
+ model: ADDIS_CHAT_MODEL2,
817
+ choices: [
818
+ {
819
+ index: 0,
820
+ message,
821
+ finish_reason: toolCalls ? "tool_calls" : normalizeFinishReason(data.finish_reason)
822
+ }
823
+ ],
824
+ usage: data.usage_metadata ? {
825
+ prompt_tokens: data.usage_metadata.prompt_token_count ?? 0,
826
+ completion_tokens: data.usage_metadata.candidates_token_count ?? 0,
827
+ total_tokens: data.usage_metadata.total_token_count ?? 0
828
+ } : void 0
829
+ };
830
+ if (data.transcription_raw || data.transcription_clean) {
831
+ completion.transcription = { raw: data.transcription_raw, clean: data.transcription_clean };
832
+ }
833
+ if (data.uploaded_attachments?.length) {
834
+ completion.uploaded_attachments = data.uploaded_attachments;
835
+ }
836
+ return completion;
837
+ }
838
+ function normalizeFinishReason(reason) {
839
+ if (typeof reason !== "string" || !reason) return "stop";
840
+ switch (reason.toUpperCase()) {
841
+ case "STOP":
842
+ return "stop";
843
+ case "MAX_TOKENS":
844
+ return "length";
845
+ case "SAFETY":
846
+ case "RECITATION":
847
+ return "content_filter";
848
+ case "TOOL_CALLS":
849
+ return "tool_calls";
850
+ default:
851
+ return reason.toLowerCase();
852
+ }
853
+ }
854
+ function cryptoRandom2() {
855
+ const c = globalThis.crypto;
856
+ if (c?.randomUUID) return c.randomUUID();
857
+ return Math.random().toString(36).slice(2);
858
+ }
859
+
860
+ // src/core/camelize.ts
861
+ function toCamel(key) {
862
+ return key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
863
+ }
864
+ function camelize(input) {
865
+ if (Array.isArray(input)) {
866
+ return input.map((item) => camelize(item));
867
+ }
868
+ if (input && typeof input === "object") {
869
+ const out = {};
870
+ for (const [key, value] of Object.entries(input)) {
871
+ out[toCamel(key)] = camelize(value);
872
+ }
873
+ return out;
874
+ }
875
+ return input;
876
+ }
877
+
878
+ // src/core/idempotency.ts
879
+ var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
880
+ var TIME_LEN = 10;
881
+ var RANDOM_LEN = 16;
882
+ function randomBytes(n) {
883
+ const buf = new Uint8Array(n);
884
+ const c = globalThis.crypto;
885
+ if (c?.getRandomValues) {
886
+ c.getRandomValues(buf);
887
+ return buf;
888
+ }
889
+ for (let i = 0; i < n; i++) buf[i] = Math.floor(Math.random() * 256);
890
+ return buf;
891
+ }
892
+ function encodeTime(now) {
893
+ let out = "";
894
+ for (let i = TIME_LEN - 1; i >= 0; i--) {
895
+ const mod = now % 32;
896
+ out = ENCODING[mod] + out;
897
+ now = (now - mod) / 32;
898
+ }
899
+ return out;
900
+ }
901
+ function encodeRandom() {
902
+ const bytes = randomBytes(RANDOM_LEN);
903
+ let out = "";
904
+ for (let i = 0; i < RANDOM_LEN; i++) {
905
+ out += ENCODING[bytes[i] % 32];
906
+ }
907
+ return out;
908
+ }
909
+ function ulid() {
910
+ return encodeTime(Date.now()) + encodeRandom();
911
+ }
912
+
913
+ // src/core/pagination.ts
914
+ var CursorPage = class {
915
+ constructor(firstPage, fetchPage) {
916
+ this.firstPage = firstPage;
917
+ this.fetchPage = fetchPage;
918
+ }
919
+ /** Items on the first page only. */
920
+ get data() {
921
+ return this.firstPage.data;
922
+ }
923
+ get nextCursor() {
924
+ return this.firstPage.nextCursor;
925
+ }
926
+ async *[Symbol.asyncIterator]() {
927
+ let page = this.firstPage;
928
+ while (true) {
929
+ for (const item of page.data) yield item;
930
+ if (!page.nextCursor) return;
931
+ page = await this.fetchPage(page.nextCursor);
932
+ }
933
+ }
934
+ };
935
+ var CursorPagePromise = class {
936
+ constructor(load) {
937
+ this.load = load;
938
+ }
939
+ get inner() {
940
+ return this.promise ??= this.load();
941
+ }
942
+ then(onfulfilled, onrejected) {
943
+ return this.inner.then(onfulfilled, onrejected);
944
+ }
945
+ catch(onrejected) {
946
+ return this.inner.catch(onrejected);
947
+ }
948
+ finally(onfinally) {
949
+ return this.inner.finally(onfinally);
950
+ }
951
+ async *[Symbol.asyncIterator]() {
952
+ const page = await this.inner;
953
+ yield* page;
954
+ }
955
+ };
956
+
957
+ // src/lib/clip.ts
958
+ var AddisClip = class {
959
+ constructor(data, fetchImpl) {
960
+ this.fetchImpl = fetchImpl;
961
+ Object.assign(this, data);
962
+ }
963
+ /** Fetch the audio bytes from the signed playback URL. */
964
+ async arrayBuffer() {
965
+ if (!this.audioUrl) throw new AddisAIError("This clip has no audioUrl to download.");
966
+ const res = await this.fetchImpl(this.audioUrl);
967
+ if (!res.ok) {
968
+ throw new AddisAIError(`Failed to download clip ${this.id}: HTTP ${res.status}.`);
969
+ }
970
+ return res.arrayBuffer();
971
+ }
972
+ /** Write the audio to a file on disk (Node/Bun/Deno). */
973
+ async toFile(path) {
974
+ const buffer = await this.arrayBuffer();
975
+ const fs = await import('fs/promises');
976
+ await fs.writeFile(path, Buffer.from(buffer));
977
+ }
978
+ };
979
+
980
+ // src/resources/voice.ts
981
+ var VOICE_TIMEOUT_FLOOR_MS = 95e3;
982
+ var Voice = class {
983
+ constructor(transport) {
984
+ this.transport = transport;
985
+ this.clips = new Clips(transport);
986
+ }
987
+ /** Synthesize speech and return the generated clip. */
988
+ async generate(params, opts = {}) {
989
+ const clientRequestId = params.clientRequestId ?? ulid();
990
+ const body = {
991
+ text: params.text,
992
+ language: params.language,
993
+ voice_id: params.voiceId,
994
+ output_format: params.outputFormat ?? "mp3_44100",
995
+ voice_settings: params.voiceSettings,
996
+ stream: false,
997
+ client_request_id: clientRequestId
998
+ };
999
+ const data = unwrapData(
1000
+ await this.transport.request(
1001
+ { method: "POST", path: "/api/v1/voice/generations", body, timeoutFloor: VOICE_TIMEOUT_FLOOR_MS },
1002
+ opts
1003
+ )
1004
+ );
1005
+ return new AddisClip({ ...mapClip(data), clientRequestId }, this.transport.fetch);
1006
+ }
1007
+ /**
1008
+ * Streaming synthesis. The surface is stable for when the API enables it;
1009
+ * until then it raises NotSupportedError. Use {@link Voice.generate} today.
1010
+ */
1011
+ async stream(_params, _opts) {
1012
+ throw new NotSupportedError(
1013
+ "Streaming voice synthesis is not yet available. Use voice.generate()."
1014
+ );
1015
+ }
1016
+ /** Pre-flight cost estimate (and whether the wallet can cover it). */
1017
+ async estimate(params, opts = {}) {
1018
+ const body = {
1019
+ text: params.text,
1020
+ language: params.language,
1021
+ voice_id: params.voiceId,
1022
+ output_format: params.outputFormat ?? "mp3_44100"
1023
+ };
1024
+ const data = unwrapData(
1025
+ await this.transport.request({ method: "POST", path: "/api/v1/voice/estimate", body }, opts)
1026
+ );
1027
+ return camelize(data);
1028
+ }
1029
+ /** Wallet balance and pricing for voice generation. */
1030
+ async usage(opts = {}) {
1031
+ const data = unwrapData(
1032
+ await this.transport.request({ method: "GET", path: "/api/v1/voice/usage" }, opts)
1033
+ );
1034
+ return camelize(data);
1035
+ }
1036
+ };
1037
+ var Clips = class {
1038
+ constructor(transport) {
1039
+ this.transport = transport;
1040
+ }
1041
+ /**
1042
+ * List generated clips. The returned value is both awaitable
1043
+ * (`const page = await clips.list()`) and async-iterable
1044
+ * (`for await (const clip of clips.list())` walks every page).
1045
+ */
1046
+ list(params = {}, opts = {}) {
1047
+ const fetchPage = async (cursor) => {
1048
+ const query = {
1049
+ limit: params.limit,
1050
+ cursor,
1051
+ language: params.language,
1052
+ voice_id: params.voiceId
1053
+ };
1054
+ const body = await this.transport.request(
1055
+ { method: "GET", path: "/api/v1/voice/clips", query },
1056
+ opts
1057
+ );
1058
+ return {
1059
+ data: (body.data ?? []).map((c) => new AddisClip(mapClip(c), this.transport.fetch)),
1060
+ nextCursor: body.meta?.next_cursor ?? null,
1061
+ limit: body.meta?.limit ?? null
1062
+ };
1063
+ };
1064
+ return new CursorPagePromise(async () => {
1065
+ const first = await fetchPage(params.cursor);
1066
+ return new CursorPage(first, (cursor) => fetchPage(cursor));
1067
+ });
1068
+ }
1069
+ /** Fetch one clip by ID. */
1070
+ async get(clipId, opts = {}) {
1071
+ const data = unwrapData(
1072
+ await this.transport.request({ method: "GET", path: `/api/v1/voice/clips/${encodeURIComponent(clipId)}` }, opts)
1073
+ );
1074
+ return new AddisClip(mapClip(data), this.transport.fetch);
1075
+ }
1076
+ /** Download the audio bytes for a clip. */
1077
+ async download(clipId, opts = {}) {
1078
+ const clip = await this.get(clipId, opts);
1079
+ return clip.arrayBuffer();
1080
+ }
1081
+ /** Delete a clip. */
1082
+ async delete(clipId, opts = {}) {
1083
+ await this.transport.request(
1084
+ { method: "DELETE", path: `/api/v1/voice/clips/${encodeURIComponent(clipId)}`, raw: true },
1085
+ opts
1086
+ );
1087
+ }
1088
+ };
1089
+ function mapClip(raw) {
1090
+ return {
1091
+ id: raw.id,
1092
+ text: raw.text ?? raw.text_preview ?? "",
1093
+ textPreview: raw.text_preview ?? "",
1094
+ voiceId: raw.voice_id,
1095
+ voiceName: raw.voice_name,
1096
+ voiceDescriptor: raw.voice_descriptor,
1097
+ language: raw.language,
1098
+ outputFormat: raw.output_format,
1099
+ audioUrl: raw.audio_url ?? raw.playback?.url ?? "",
1100
+ mimeType: raw.mime_type,
1101
+ durationSeconds: raw.duration_seconds ?? null,
1102
+ characterCount: raw.character_count ?? 0,
1103
+ billableCharacters: raw.billable_characters ?? 0,
1104
+ downloadName: raw.download_name ?? "",
1105
+ createdAt: raw.created_at,
1106
+ usage: raw.usage ? {
1107
+ pricingUnit: raw.usage.pricing_unit ?? "character",
1108
+ pricePer1000Characters: raw.usage.price_per_1000_characters ?? 0,
1109
+ creditsUsed: raw.usage.credits_used ?? null,
1110
+ creditsRemaining: raw.usage.credits_remaining ?? null,
1111
+ currency: raw.usage.currency ?? "ETB"
1112
+ } : void 0,
1113
+ meta: raw.meta ? {
1114
+ ignoredVoiceSettings: raw.meta.ignored_voice_settings ?? [],
1115
+ idempotentReplay: Boolean(raw.meta.idempotent_replay)
1116
+ } : void 0
1117
+ };
1118
+ }
1119
+
1120
+ // src/resources/voices.ts
1121
+ var Voices = class {
1122
+ constructor(transport) {
1123
+ this.transport = transport;
1124
+ }
1125
+ /** List the voice catalog. */
1126
+ async list(params = {}, opts = {}) {
1127
+ const query = {
1128
+ language: params.language,
1129
+ gender: params.gender,
1130
+ search: params.search,
1131
+ include_unavailable: params.includeUnavailable
1132
+ };
1133
+ const data = unwrapData(
1134
+ await this.transport.request({ method: "GET", path: "/api/v1/voice/voices", query }, opts)
1135
+ );
1136
+ return camelize(data);
1137
+ }
1138
+ /** Fetch a single voice's preview clip metadata. */
1139
+ async preview(voiceId, opts = {}) {
1140
+ const data = unwrapData(
1141
+ await this.transport.request(
1142
+ { method: "GET", path: `/api/v1/voice/voices/${encodeURIComponent(voiceId)}/preview` },
1143
+ opts
1144
+ )
1145
+ );
1146
+ return camelize(data);
1147
+ }
1148
+ };
1149
+
1150
+ // src/resources/speech.ts
1151
+ var Speech = class {
1152
+ constructor(transport) {
1153
+ this.transport = transport;
1154
+ }
1155
+ /** Transcribe speech to text. */
1156
+ async transcribe(params, opts = {}) {
1157
+ const form = new FormData();
1158
+ const { blob, filename } = toBlob(params.audio, "audio/wav");
1159
+ form.append("audio", blob, filename || "audio.wav");
1160
+ form.append(
1161
+ "request_data",
1162
+ new Blob([JSON.stringify({ language_code: params.language })], { type: "application/json" })
1163
+ );
1164
+ const data = unwrapData(
1165
+ await this.transport.request({ method: "POST", path: "/api/v2/stt", form }, opts)
1166
+ );
1167
+ return {
1168
+ text: data.transcription ?? "",
1169
+ confidence: data.confidence ?? null,
1170
+ usage: data.usage_metadata ?? null
1171
+ };
1172
+ }
1173
+ };
1174
+
1175
+ // src/resources/translate.ts
1176
+ var Translate = class {
1177
+ constructor(transport) {
1178
+ this.transport = transport;
1179
+ }
1180
+ /** Translate text between Amharic, Afan Oromo, and English. */
1181
+ async create(params, opts = {}) {
1182
+ const body = {
1183
+ text: params.text,
1184
+ source_language: params.from,
1185
+ target_language: params.to
1186
+ };
1187
+ const data = unwrapData(await this.transport.request({ method: "POST", path: "/api/v1/translate", body }, opts));
1188
+ return {
1189
+ text: data.translation ?? "",
1190
+ sourceLanguage: data.source_language ?? params.from,
1191
+ targetLanguage: data.target_language ?? params.to,
1192
+ quality: data.quality ?? null,
1193
+ usage: data.usage_metadata ?? null
1194
+ };
1195
+ }
1196
+ };
1197
+
1198
+ // src/resources/text-to-speech.ts
1199
+ var TextToSpeech = class {
1200
+ constructor(voice) {
1201
+ this.voice = voice;
1202
+ }
1203
+ /** Synthesize speech for a voice. Mirrors ElevenLabs `textToSpeech.convert`. */
1204
+ convert(voiceId, params, opts) {
1205
+ return this.voice.generate({ voiceId, ...params }, opts);
1206
+ }
1207
+ };
1208
+
1209
+ // src/lib/audio-stream.ts
1210
+ var AudioStream = class _AudioStream {
1211
+ constructor(response, mode, controller) {
1212
+ this.response = response;
1213
+ this.mode = mode;
1214
+ this.controller = controller;
1215
+ this.consumed = false;
1216
+ }
1217
+ static fromResponse(response, controller) {
1218
+ const contentType = (response.headers.get("content-type") || "").toLowerCase();
1219
+ const mode = contentType.includes("ndjson") || contentType.includes("json") ? "ndjson" : "raw";
1220
+ return new _AudioStream(response, mode, controller);
1221
+ }
1222
+ /** Cancel the stream and underlying request. */
1223
+ abort() {
1224
+ this.controller?.abort();
1225
+ }
1226
+ async *[Symbol.asyncIterator]() {
1227
+ if (this.consumed) throw new AddisAIError("This stream has already been consumed.");
1228
+ this.consumed = true;
1229
+ if (!this.response.body) return;
1230
+ if (this.mode === "raw") {
1231
+ const reader = this.response.body.getReader();
1232
+ try {
1233
+ while (true) {
1234
+ const { done, value } = await reader.read();
1235
+ if (done) break;
1236
+ if (value) yield value;
1237
+ }
1238
+ } finally {
1239
+ reader.releaseLock();
1240
+ }
1241
+ return;
1242
+ }
1243
+ for await (const obj of parseNDJSON(this.response.body)) {
1244
+ if (!obj) continue;
1245
+ if (obj.status === "error" || obj.error) {
1246
+ const message = obj.error?.message || "Audio stream failed.";
1247
+ throw new AddisAIError(message);
1248
+ }
1249
+ const b64 = obj.audio_chunk ?? obj.audio;
1250
+ if (typeof b64 === "string" && b64) yield decodeBase64(b64);
1251
+ }
1252
+ }
1253
+ /** Collect every chunk into a single ArrayBuffer. */
1254
+ async arrayBuffer() {
1255
+ const chunks = [];
1256
+ let total = 0;
1257
+ for await (const chunk of this) {
1258
+ chunks.push(chunk);
1259
+ total += chunk.byteLength;
1260
+ }
1261
+ const out = new Uint8Array(total);
1262
+ let offset = 0;
1263
+ for (const c of chunks) {
1264
+ out.set(c, offset);
1265
+ offset += c.byteLength;
1266
+ }
1267
+ return out.buffer;
1268
+ }
1269
+ /** Stream the audio to a file on disk (Node/Bun/Deno). */
1270
+ async toFile(path) {
1271
+ const fs = await import('fs');
1272
+ const handle = fs.createWriteStream(path);
1273
+ try {
1274
+ for await (const chunk of this) {
1275
+ await new Promise((resolve, reject) => {
1276
+ handle.write(Buffer.from(chunk), (err) => err ? reject(err) : resolve());
1277
+ });
1278
+ }
1279
+ } finally {
1280
+ await new Promise((resolve) => handle.end(resolve));
1281
+ }
1282
+ }
1283
+ };
1284
+ function decodeBase64(b64) {
1285
+ if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(b64, "base64"));
1286
+ const binary = atob(b64);
1287
+ const bytes = new Uint8Array(binary.length);
1288
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1289
+ return bytes;
1290
+ }
1291
+
1292
+ // src/resources/legacy.ts
1293
+ var warned = false;
1294
+ function warnOnce() {
1295
+ if (warned) return;
1296
+ warned = true;
1297
+ console.warn(
1298
+ "[addisai] addis.legacy.audio is DEPRECATED. Migrate to addis.voice.generate(), which uses the current, more capable voice model. The legacy /audio endpoint may be removed in a future release."
1299
+ );
1300
+ }
1301
+ function decodeBase642(b64) {
1302
+ if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(b64, "base64"));
1303
+ const binary = atob(b64);
1304
+ const bytes = new Uint8Array(binary.length);
1305
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1306
+ return bytes;
1307
+ }
1308
+ var LegacyAudio = class {
1309
+ constructor(audio) {
1310
+ this.audio = audio;
1311
+ }
1312
+ /** Decode the base64 audio into bytes. */
1313
+ arrayBuffer() {
1314
+ const bytes = decodeBase642(this.audio);
1315
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1316
+ }
1317
+ /** Write the decoded audio to disk (Node/Bun/Deno). */
1318
+ async toFile(path) {
1319
+ const fs = await import('fs/promises');
1320
+ await fs.writeFile(path, Buffer.from(this.arrayBuffer()));
1321
+ }
1322
+ };
1323
+ var LegacyAudioResource = class {
1324
+ constructor(transport) {
1325
+ this.transport = transport;
1326
+ }
1327
+ /**
1328
+ * @deprecated Use `addis.voice.generate(...)`.
1329
+ * Synthesize speech via the legacy endpoint (non-streaming).
1330
+ */
1331
+ async generate(params, opts = {}) {
1332
+ warnOnce();
1333
+ const body = await this.transport.request(
1334
+ { method: "POST", path: "/api/v1/audio", body: { text: params.text, language: params.language, stream: false } },
1335
+ opts
1336
+ );
1337
+ const audio = body.audio ?? body.audio_chunk;
1338
+ if (!audio) throw new AddisAIError("Legacy audio response did not contain audio data.");
1339
+ return new LegacyAudio(audio);
1340
+ }
1341
+ /**
1342
+ * @deprecated Use `addis.voice.generate(...)`.
1343
+ * Stream synthesis via the legacy endpoint. Returns an {@link AudioStream} of
1344
+ * audio byte chunks. The SDK normalizes the two legacy encodings (Amharic
1345
+ * newline-delimited base64 JSON, Afan Oromo raw `audio/wav`) into one byte
1346
+ * stream. New code should not depend on this.
1347
+ */
1348
+ async stream(params, opts = {}) {
1349
+ warnOnce();
1350
+ const { response, controller } = await this.transport.openStream(
1351
+ { method: "POST", path: "/api/v1/audio", body: { text: params.text, language: params.language, stream: true } },
1352
+ opts
1353
+ );
1354
+ return AudioStream.fromResponse(response, controller);
1355
+ }
1356
+ };
1357
+ var Legacy = class {
1358
+ constructor(transport) {
1359
+ this.audio = new LegacyAudioResource(transport);
1360
+ }
1361
+ };
1362
+
1363
+ // src/client.ts
1364
+ var AddisAI = class {
1365
+ constructor(options = {}) {
1366
+ const apiKey = options.apiKey ?? readEnv(API_KEY_ENV_VAR);
1367
+ if (!apiKey) {
1368
+ throw new AddisAIError(
1369
+ `Missing API key. Pass { apiKey } or set the ${API_KEY_ENV_VAR} environment variable.`
1370
+ );
1371
+ }
1372
+ if (isBrowserLike() && !options.dangerouslyAllowBrowser) {
1373
+ throw new AddisAIError(
1374
+ "It looks like you're running in a browser. Exposing an Addis AI API key in client-side code is a security risk. Call the SDK from your server, or pass { dangerouslyAllowBrowser: true } if you understand the risk."
1375
+ );
1376
+ }
1377
+ const resolvedFetch = options.fetch ?? globalThis.fetch;
1378
+ if (!resolvedFetch) {
1379
+ throw new AddisAIError(
1380
+ `No fetch implementation found in this runtime (${detectRuntime()}). Upgrade to Node 18+ or pass a custom fetch via { fetch }.`
1381
+ );
1382
+ }
1383
+ this._apiKey = apiKey;
1384
+ this._transport = new Transport({
1385
+ apiKey,
1386
+ baseURL: resolveBaseURL(options.baseURL),
1387
+ timeout: options.timeout ?? 6e4,
1388
+ maxRetries: options.maxRetries ?? 3,
1389
+ defaultHeaders: options.defaultHeaders ?? {},
1390
+ defaultQuery: options.defaultQuery ?? {},
1391
+ fetch: resolvedFetch,
1392
+ logLevel: options.logLevel ?? "warn"
1393
+ });
1394
+ this.chat = new Chat(this._transport);
1395
+ this.voice = new Voice(this._transport);
1396
+ this.voices = new Voices(this._transport);
1397
+ this.speech = new Speech(this._transport);
1398
+ this.translate = new Translate(this._transport);
1399
+ this.textToSpeech = new TextToSpeech(this.voice);
1400
+ this.legacy = new Legacy(this._transport);
1401
+ }
1402
+ /** Redacted representation; never exposes the API key. */
1403
+ toString() {
1404
+ return `AddisAI { apiKey: "${redactApiKey(this._apiKey)}" }`;
1405
+ }
1406
+ };
1407
+
1408
+ // src/lib/play.ts
1409
+ async function play(audio) {
1410
+ const bytes = audio instanceof AddisClip ? new Uint8Array(await audio.arrayBuffer()) : audio instanceof ArrayBuffer ? new Uint8Array(audio) : audio;
1411
+ const { spawn } = await import('child_process');
1412
+ const tryPlayer = (command, args) => new Promise((resolve) => {
1413
+ let child;
1414
+ try {
1415
+ child = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"] });
1416
+ } catch {
1417
+ return resolve(false);
1418
+ }
1419
+ child.on("error", () => resolve(false));
1420
+ child.on("close", (code) => resolve(code === 0));
1421
+ child.stdin?.on("error", () => {
1422
+ });
1423
+ child.stdin?.write(Buffer.from(bytes));
1424
+ child.stdin?.end();
1425
+ });
1426
+ if (await tryPlayer("ffplay", ["-autoexit", "-nodisp", "-loglevel", "quiet", "-"])) return;
1427
+ if (await tryPlayer("mpv", ["--really-quiet", "-"])) return;
1428
+ throw new Error(
1429
+ "play() requires ffplay (ffmpeg) or mpv on PATH. Save the clip with clip.toFile() instead."
1430
+ );
1431
+ }
1432
+
1433
+ // src/index.ts
1434
+ var index_default = AddisAI;
1435
+
1436
+ exports.ADDIS_CHAT_MODEL = ADDIS_CHAT_MODEL2;
1437
+ exports.APIConnectionError = APIConnectionError;
1438
+ exports.APIConnectionTimeoutError = APIConnectionTimeoutError;
1439
+ exports.APIError = APIError;
1440
+ exports.AddisAI = AddisAI;
1441
+ exports.AddisAIError = AddisAIError;
1442
+ exports.AddisClip = AddisClip;
1443
+ exports.AudioStream = AudioStream;
1444
+ exports.AuthenticationError = AuthenticationError;
1445
+ exports.BadRequestError = BadRequestError;
1446
+ exports.ChatStream = ChatStream;
1447
+ exports.ConflictError = ConflictError;
1448
+ exports.CursorPage = CursorPage;
1449
+ exports.CursorPagePromise = CursorPagePromise;
1450
+ exports.GenerationInProgressError = GenerationInProgressError;
1451
+ exports.IdempotencyConflictError = IdempotencyConflictError;
1452
+ exports.InsufficientCreditsError = InsufficientCreditsError;
1453
+ exports.InternalServerError = InternalServerError;
1454
+ exports.LegacyAudio = LegacyAudio;
1455
+ exports.NotFoundError = NotFoundError;
1456
+ exports.NotSupportedError = NotSupportedError;
1457
+ exports.PermissionDeniedError = PermissionDeniedError;
1458
+ exports.RateLimitError = RateLimitError;
1459
+ exports.UnprocessableEntityError = UnprocessableEntityError;
1460
+ exports.default = index_default;
1461
+ exports.fileFromPath = fileFromPath;
1462
+ exports.play = play;
1463
+ exports.ulid = ulid;
1464
+ //# sourceMappingURL=index.cjs.map
1465
+ //# sourceMappingURL=index.cjs.map