kugelaudio 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -22,10 +22,13 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AuthenticationError: () => AuthenticationError,
24
24
  ConnectionError: () => ConnectionError,
25
+ DictionariesResource: () => DictionariesResource,
26
+ DictionaryEntriesResource: () => DictionaryEntriesResource,
25
27
  ErrorCodes: () => ErrorCodes,
26
28
  InsufficientCreditsError: () => InsufficientCreditsError,
27
29
  KugelAudio: () => KugelAudio,
28
30
  KugelAudioError: () => KugelAudioError,
31
+ NotFoundError: () => NotFoundError,
29
32
  RateLimitError: () => RateLimitError,
30
33
  ValidationError: () => ValidationError,
31
34
  WsCloseCodes: () => WsCloseCodes,
@@ -40,6 +43,187 @@ __export(index_exports, {
40
43
  });
41
44
  module.exports = __toCommonJS(index_exports);
42
45
 
46
+ // src/dictionaries.ts
47
+ function mapDictionary(raw) {
48
+ return {
49
+ id: raw.id,
50
+ projectId: raw.project_id,
51
+ name: raw.name,
52
+ description: raw.description ?? void 0,
53
+ language: raw.language ?? void 0,
54
+ isActive: raw.is_active ?? true,
55
+ createdAt: raw.created_at,
56
+ updatedAt: raw.updated_at
57
+ };
58
+ }
59
+ function mapEntry(raw) {
60
+ return {
61
+ id: raw.id,
62
+ dictionaryId: raw.dictionary_id,
63
+ word: raw.word,
64
+ replacement: raw.replacement,
65
+ ipa: raw.ipa ?? void 0,
66
+ caseSensitive: raw.case_sensitive ?? false,
67
+ createdAt: raw.created_at,
68
+ updatedAt: raw.updated_at
69
+ };
70
+ }
71
+ function entryPayload(input) {
72
+ const payload = {
73
+ word: input.word,
74
+ replacement: input.replacement
75
+ };
76
+ if (input.ipa !== void 0) payload.ipa = input.ipa;
77
+ if (input.caseSensitive !== void 0) {
78
+ payload.case_sensitive = input.caseSensitive;
79
+ }
80
+ return payload;
81
+ }
82
+ function buildPath(base, params) {
83
+ const search = new URLSearchParams();
84
+ for (const [key, val] of Object.entries(params)) {
85
+ if (val === void 0 || val === null) continue;
86
+ search.set(key, String(val));
87
+ }
88
+ const query = search.toString();
89
+ return query ? `${base}?${query}` : base;
90
+ }
91
+ var DictionaryEntriesResource = class {
92
+ constructor(client) {
93
+ this.client = client;
94
+ }
95
+ /** List entries with optional search + pagination. */
96
+ async list(dictionaryId, options) {
97
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries`, {
98
+ search: options?.search,
99
+ limit: options?.limit,
100
+ offset: options?.offset,
101
+ project_id: options?.projectId
102
+ });
103
+ const raw = await this.client.request("GET", path);
104
+ return {
105
+ entries: raw.entries.map(mapEntry),
106
+ total: raw.total,
107
+ limit: raw.limit,
108
+ offset: raw.offset
109
+ };
110
+ }
111
+ /** Add a single entry to a dictionary. */
112
+ async add(dictionaryId, entry, options) {
113
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries`, {
114
+ project_id: options?.projectId
115
+ });
116
+ const raw = await this.client.request(
117
+ "POST",
118
+ path,
119
+ entryPayload(entry)
120
+ );
121
+ return mapEntry(raw);
122
+ }
123
+ /** Update an existing entry. */
124
+ async update(dictionaryId, entryId, updates, options) {
125
+ const payload = {};
126
+ if (updates.word !== void 0) payload.word = updates.word;
127
+ if (updates.replacement !== void 0) payload.replacement = updates.replacement;
128
+ if (updates.ipa !== void 0) payload.ipa = updates.ipa;
129
+ if (updates.caseSensitive !== void 0) {
130
+ payload.case_sensitive = updates.caseSensitive;
131
+ }
132
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries/${entryId}`, {
133
+ project_id: options?.projectId
134
+ });
135
+ const raw = await this.client.request(
136
+ "PATCH",
137
+ path,
138
+ payload
139
+ );
140
+ return mapEntry(raw);
141
+ }
142
+ /** Delete a single entry. */
143
+ async delete(dictionaryId, entryId, options) {
144
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries/${entryId}`, {
145
+ project_id: options?.projectId
146
+ });
147
+ await this.client.request("DELETE", path);
148
+ }
149
+ /**
150
+ * Replace every entry in the dictionary atomically.
151
+ *
152
+ * Each item must have ``word`` and ``replacement``; existing entries
153
+ * whose ``word`` is not in the supplied list are deleted. Idempotent.
154
+ */
155
+ async replaceAll(dictionaryId, entries, options) {
156
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries`, {
157
+ project_id: options?.projectId
158
+ });
159
+ const raw = await this.client.request("PUT", path, {
160
+ entries: entries.map(entryPayload)
161
+ });
162
+ return raw;
163
+ }
164
+ };
165
+ var DictionariesResource = class {
166
+ constructor(client) {
167
+ this.client = client;
168
+ this.entries = new DictionaryEntriesResource(client);
169
+ }
170
+ /** List every dictionary in the caller's project. */
171
+ async list(options) {
172
+ const path = buildPath("/v1/dictionaries", {
173
+ project_id: options?.projectId
174
+ });
175
+ const raw = await this.client.request("GET", path);
176
+ return raw.dictionaries.map(mapDictionary);
177
+ }
178
+ /** Create a new dictionary scoped to the caller's project. */
179
+ async create(body, options) {
180
+ const path = buildPath("/v1/dictionaries", {
181
+ project_id: options?.projectId
182
+ });
183
+ const payload = { name: body.name };
184
+ if (body.description !== void 0) payload.description = body.description;
185
+ if (body.language !== void 0) payload.language = body.language;
186
+ const raw = await this.client.request(
187
+ "POST",
188
+ path,
189
+ payload
190
+ );
191
+ return mapDictionary(raw);
192
+ }
193
+ /** Fetch a single dictionary. */
194
+ async get(dictionaryId, options) {
195
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}`, {
196
+ project_id: options?.projectId
197
+ });
198
+ const raw = await this.client.request("GET", path);
199
+ return mapDictionary(raw);
200
+ }
201
+ /** Update name / description / language / isActive. */
202
+ async update(dictionaryId, updates, options) {
203
+ const payload = {};
204
+ if (updates.name !== void 0) payload.name = updates.name;
205
+ if (updates.description !== void 0) payload.description = updates.description;
206
+ if (updates.language !== void 0) payload.language = updates.language;
207
+ if (updates.isActive !== void 0) payload.is_active = updates.isActive;
208
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}`, {
209
+ project_id: options?.projectId
210
+ });
211
+ const raw = await this.client.request(
212
+ "PATCH",
213
+ path,
214
+ payload
215
+ );
216
+ return mapDictionary(raw);
217
+ }
218
+ /** Delete a dictionary (cascades to its entries). */
219
+ async delete(dictionaryId, options) {
220
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}`, {
221
+ project_id: options?.projectId
222
+ });
223
+ await this.client.request("DELETE", path);
224
+ }
225
+ };
226
+
43
227
  // src/errors.ts
44
228
  var ErrorCodes = {
45
229
  UNAUTHORIZED: "UNAUTHORIZED",
@@ -112,6 +296,17 @@ var ConnectionError = class _ConnectionError extends KugelAudioError {
112
296
  Object.setPrototypeOf(this, _ConnectionError.prototype);
113
297
  }
114
298
  };
299
+ var NotFoundError = class _NotFoundError extends KugelAudioError {
300
+ constructor(message, options = {}) {
301
+ super(message ?? "Not found.", {
302
+ statusCode: 404,
303
+ errorCode: ErrorCodes.NOT_FOUND,
304
+ ...options
305
+ });
306
+ this.name = "NotFoundError";
307
+ Object.setPrototypeOf(this, _NotFoundError.prototype);
308
+ }
309
+ };
115
310
  function build(status, errorCode, message, opts = {}) {
116
311
  const common = { ...opts };
117
312
  if (status !== void 0) common.statusCode = status;
@@ -135,6 +330,9 @@ function build(status, errorCode, message, opts = {}) {
135
330
  common
136
331
  );
137
332
  }
333
+ if (errorCode === ErrorCodes.NOT_FOUND || status === 404) {
334
+ return new NotFoundError(message || void 0, common);
335
+ }
138
336
  return new KugelAudioError(message || `HTTP ${status}`, common);
139
337
  }
140
338
  function readHeader(headers, name) {
@@ -312,11 +510,9 @@ function getWebSocket() {
312
510
  }
313
511
 
314
512
  // src/client.ts
315
- var REGION_URLS = {
316
- eu: "https://api.kugelaudio.com",
317
- us: "https://us-api.kugelaudio.com",
318
- global: "https://global-api.kugelaudio.com"
319
- };
513
+ var DEFAULT_API_URL = "https://api.kugelaudio.com";
514
+ var EU_API_URL = "https://api.eu.kugelaudio.com";
515
+ var SUPPORTED_REGIONS = ["eu", "us", "global"];
320
516
  var REGION_PREFIXES = ["eu-", "us-", "global-"];
321
517
  function parseApiKey(apiKey) {
322
518
  for (const prefix of REGION_PREFIXES) {
@@ -1539,19 +1735,23 @@ var KugelAudio = class _KugelAudio {
1539
1735
  if (options.apiUrl) {
1540
1736
  this._apiUrl = options.apiUrl.replace(/\/$/, "");
1541
1737
  } else {
1542
- const effectiveRegion = options.region || detectedRegion || "eu";
1543
- if (!(effectiveRegion in REGION_URLS)) {
1738
+ const effectiveRegion = options.region || detectedRegion;
1739
+ if (!effectiveRegion) {
1740
+ this._apiUrl = DEFAULT_API_URL;
1741
+ } else if (!SUPPORTED_REGIONS.includes(effectiveRegion)) {
1544
1742
  throw new ValidationError(
1545
- `Invalid region '${effectiveRegion}'. Must be one of: ${Object.keys(REGION_URLS).join(", ")}.`
1743
+ `Invalid region '${effectiveRegion}'. Must be one of: ${SUPPORTED_REGIONS.join(", ")}.`
1546
1744
  );
1745
+ } else {
1746
+ this._apiUrl = effectiveRegion === "eu" ? EU_API_URL : DEFAULT_API_URL;
1547
1747
  }
1548
- this._apiUrl = REGION_URLS[effectiveRegion];
1549
1748
  }
1550
1749
  this._ttsUrl = (options.ttsUrl || this._apiUrl).replace(/\/$/, "");
1551
1750
  this._timeout = options.timeout || 6e4;
1552
1751
  this._keepalivePingInterval = options.keepalivePingInterval !== void 0 ? options.keepalivePingInterval : 2e4;
1553
1752
  this.models = new ModelsResource(this);
1554
1753
  this.voices = new VoicesResource(this);
1754
+ this.dictionaries = new DictionariesResource(this);
1555
1755
  this.tts = new TTSResource(this);
1556
1756
  }
1557
1757
  /**
@@ -1718,10 +1918,13 @@ var KugelAudio = class _KugelAudio {
1718
1918
  0 && (module.exports = {
1719
1919
  AuthenticationError,
1720
1920
  ConnectionError,
1921
+ DictionariesResource,
1922
+ DictionaryEntriesResource,
1721
1923
  ErrorCodes,
1722
1924
  InsufficientCreditsError,
1723
1925
  KugelAudio,
1724
1926
  KugelAudioError,
1927
+ NotFoundError,
1725
1928
  RateLimitError,
1726
1929
  ValidationError,
1727
1930
  WsCloseCodes,
package/dist/index.mjs CHANGED
@@ -5,6 +5,187 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
5
  throw Error('Dynamic require of "' + x + '" is not supported');
6
6
  });
7
7
 
8
+ // src/dictionaries.ts
9
+ function mapDictionary(raw) {
10
+ return {
11
+ id: raw.id,
12
+ projectId: raw.project_id,
13
+ name: raw.name,
14
+ description: raw.description ?? void 0,
15
+ language: raw.language ?? void 0,
16
+ isActive: raw.is_active ?? true,
17
+ createdAt: raw.created_at,
18
+ updatedAt: raw.updated_at
19
+ };
20
+ }
21
+ function mapEntry(raw) {
22
+ return {
23
+ id: raw.id,
24
+ dictionaryId: raw.dictionary_id,
25
+ word: raw.word,
26
+ replacement: raw.replacement,
27
+ ipa: raw.ipa ?? void 0,
28
+ caseSensitive: raw.case_sensitive ?? false,
29
+ createdAt: raw.created_at,
30
+ updatedAt: raw.updated_at
31
+ };
32
+ }
33
+ function entryPayload(input) {
34
+ const payload = {
35
+ word: input.word,
36
+ replacement: input.replacement
37
+ };
38
+ if (input.ipa !== void 0) payload.ipa = input.ipa;
39
+ if (input.caseSensitive !== void 0) {
40
+ payload.case_sensitive = input.caseSensitive;
41
+ }
42
+ return payload;
43
+ }
44
+ function buildPath(base, params) {
45
+ const search = new URLSearchParams();
46
+ for (const [key, val] of Object.entries(params)) {
47
+ if (val === void 0 || val === null) continue;
48
+ search.set(key, String(val));
49
+ }
50
+ const query = search.toString();
51
+ return query ? `${base}?${query}` : base;
52
+ }
53
+ var DictionaryEntriesResource = class {
54
+ constructor(client) {
55
+ this.client = client;
56
+ }
57
+ /** List entries with optional search + pagination. */
58
+ async list(dictionaryId, options) {
59
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries`, {
60
+ search: options?.search,
61
+ limit: options?.limit,
62
+ offset: options?.offset,
63
+ project_id: options?.projectId
64
+ });
65
+ const raw = await this.client.request("GET", path);
66
+ return {
67
+ entries: raw.entries.map(mapEntry),
68
+ total: raw.total,
69
+ limit: raw.limit,
70
+ offset: raw.offset
71
+ };
72
+ }
73
+ /** Add a single entry to a dictionary. */
74
+ async add(dictionaryId, entry, options) {
75
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries`, {
76
+ project_id: options?.projectId
77
+ });
78
+ const raw = await this.client.request(
79
+ "POST",
80
+ path,
81
+ entryPayload(entry)
82
+ );
83
+ return mapEntry(raw);
84
+ }
85
+ /** Update an existing entry. */
86
+ async update(dictionaryId, entryId, updates, options) {
87
+ const payload = {};
88
+ if (updates.word !== void 0) payload.word = updates.word;
89
+ if (updates.replacement !== void 0) payload.replacement = updates.replacement;
90
+ if (updates.ipa !== void 0) payload.ipa = updates.ipa;
91
+ if (updates.caseSensitive !== void 0) {
92
+ payload.case_sensitive = updates.caseSensitive;
93
+ }
94
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries/${entryId}`, {
95
+ project_id: options?.projectId
96
+ });
97
+ const raw = await this.client.request(
98
+ "PATCH",
99
+ path,
100
+ payload
101
+ );
102
+ return mapEntry(raw);
103
+ }
104
+ /** Delete a single entry. */
105
+ async delete(dictionaryId, entryId, options) {
106
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries/${entryId}`, {
107
+ project_id: options?.projectId
108
+ });
109
+ await this.client.request("DELETE", path);
110
+ }
111
+ /**
112
+ * Replace every entry in the dictionary atomically.
113
+ *
114
+ * Each item must have ``word`` and ``replacement``; existing entries
115
+ * whose ``word`` is not in the supplied list are deleted. Idempotent.
116
+ */
117
+ async replaceAll(dictionaryId, entries, options) {
118
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}/entries`, {
119
+ project_id: options?.projectId
120
+ });
121
+ const raw = await this.client.request("PUT", path, {
122
+ entries: entries.map(entryPayload)
123
+ });
124
+ return raw;
125
+ }
126
+ };
127
+ var DictionariesResource = class {
128
+ constructor(client) {
129
+ this.client = client;
130
+ this.entries = new DictionaryEntriesResource(client);
131
+ }
132
+ /** List every dictionary in the caller's project. */
133
+ async list(options) {
134
+ const path = buildPath("/v1/dictionaries", {
135
+ project_id: options?.projectId
136
+ });
137
+ const raw = await this.client.request("GET", path);
138
+ return raw.dictionaries.map(mapDictionary);
139
+ }
140
+ /** Create a new dictionary scoped to the caller's project. */
141
+ async create(body, options) {
142
+ const path = buildPath("/v1/dictionaries", {
143
+ project_id: options?.projectId
144
+ });
145
+ const payload = { name: body.name };
146
+ if (body.description !== void 0) payload.description = body.description;
147
+ if (body.language !== void 0) payload.language = body.language;
148
+ const raw = await this.client.request(
149
+ "POST",
150
+ path,
151
+ payload
152
+ );
153
+ return mapDictionary(raw);
154
+ }
155
+ /** Fetch a single dictionary. */
156
+ async get(dictionaryId, options) {
157
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}`, {
158
+ project_id: options?.projectId
159
+ });
160
+ const raw = await this.client.request("GET", path);
161
+ return mapDictionary(raw);
162
+ }
163
+ /** Update name / description / language / isActive. */
164
+ async update(dictionaryId, updates, options) {
165
+ const payload = {};
166
+ if (updates.name !== void 0) payload.name = updates.name;
167
+ if (updates.description !== void 0) payload.description = updates.description;
168
+ if (updates.language !== void 0) payload.language = updates.language;
169
+ if (updates.isActive !== void 0) payload.is_active = updates.isActive;
170
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}`, {
171
+ project_id: options?.projectId
172
+ });
173
+ const raw = await this.client.request(
174
+ "PATCH",
175
+ path,
176
+ payload
177
+ );
178
+ return mapDictionary(raw);
179
+ }
180
+ /** Delete a dictionary (cascades to its entries). */
181
+ async delete(dictionaryId, options) {
182
+ const path = buildPath(`/v1/dictionaries/${dictionaryId}`, {
183
+ project_id: options?.projectId
184
+ });
185
+ await this.client.request("DELETE", path);
186
+ }
187
+ };
188
+
8
189
  // src/errors.ts
9
190
  var ErrorCodes = {
10
191
  UNAUTHORIZED: "UNAUTHORIZED",
@@ -77,6 +258,17 @@ var ConnectionError = class _ConnectionError extends KugelAudioError {
77
258
  Object.setPrototypeOf(this, _ConnectionError.prototype);
78
259
  }
79
260
  };
261
+ var NotFoundError = class _NotFoundError extends KugelAudioError {
262
+ constructor(message, options = {}) {
263
+ super(message ?? "Not found.", {
264
+ statusCode: 404,
265
+ errorCode: ErrorCodes.NOT_FOUND,
266
+ ...options
267
+ });
268
+ this.name = "NotFoundError";
269
+ Object.setPrototypeOf(this, _NotFoundError.prototype);
270
+ }
271
+ };
80
272
  function build(status, errorCode, message, opts = {}) {
81
273
  const common = { ...opts };
82
274
  if (status !== void 0) common.statusCode = status;
@@ -100,6 +292,9 @@ function build(status, errorCode, message, opts = {}) {
100
292
  common
101
293
  );
102
294
  }
295
+ if (errorCode === ErrorCodes.NOT_FOUND || status === 404) {
296
+ return new NotFoundError(message || void 0, common);
297
+ }
103
298
  return new KugelAudioError(message || `HTTP ${status}`, common);
104
299
  }
105
300
  function readHeader(headers, name) {
@@ -277,11 +472,9 @@ function getWebSocket() {
277
472
  }
278
473
 
279
474
  // src/client.ts
280
- var REGION_URLS = {
281
- eu: "https://api.kugelaudio.com",
282
- us: "https://us-api.kugelaudio.com",
283
- global: "https://global-api.kugelaudio.com"
284
- };
475
+ var DEFAULT_API_URL = "https://api.kugelaudio.com";
476
+ var EU_API_URL = "https://api.eu.kugelaudio.com";
477
+ var SUPPORTED_REGIONS = ["eu", "us", "global"];
285
478
  var REGION_PREFIXES = ["eu-", "us-", "global-"];
286
479
  function parseApiKey(apiKey) {
287
480
  for (const prefix of REGION_PREFIXES) {
@@ -1504,19 +1697,23 @@ var KugelAudio = class _KugelAudio {
1504
1697
  if (options.apiUrl) {
1505
1698
  this._apiUrl = options.apiUrl.replace(/\/$/, "");
1506
1699
  } else {
1507
- const effectiveRegion = options.region || detectedRegion || "eu";
1508
- if (!(effectiveRegion in REGION_URLS)) {
1700
+ const effectiveRegion = options.region || detectedRegion;
1701
+ if (!effectiveRegion) {
1702
+ this._apiUrl = DEFAULT_API_URL;
1703
+ } else if (!SUPPORTED_REGIONS.includes(effectiveRegion)) {
1509
1704
  throw new ValidationError(
1510
- `Invalid region '${effectiveRegion}'. Must be one of: ${Object.keys(REGION_URLS).join(", ")}.`
1705
+ `Invalid region '${effectiveRegion}'. Must be one of: ${SUPPORTED_REGIONS.join(", ")}.`
1511
1706
  );
1707
+ } else {
1708
+ this._apiUrl = effectiveRegion === "eu" ? EU_API_URL : DEFAULT_API_URL;
1512
1709
  }
1513
- this._apiUrl = REGION_URLS[effectiveRegion];
1514
1710
  }
1515
1711
  this._ttsUrl = (options.ttsUrl || this._apiUrl).replace(/\/$/, "");
1516
1712
  this._timeout = options.timeout || 6e4;
1517
1713
  this._keepalivePingInterval = options.keepalivePingInterval !== void 0 ? options.keepalivePingInterval : 2e4;
1518
1714
  this.models = new ModelsResource(this);
1519
1715
  this.voices = new VoicesResource(this);
1716
+ this.dictionaries = new DictionariesResource(this);
1520
1717
  this.tts = new TTSResource(this);
1521
1718
  }
1522
1719
  /**
@@ -1682,10 +1879,13 @@ var KugelAudio = class _KugelAudio {
1682
1879
  export {
1683
1880
  AuthenticationError,
1684
1881
  ConnectionError,
1882
+ DictionariesResource,
1883
+ DictionaryEntriesResource,
1685
1884
  ErrorCodes,
1686
1885
  InsufficientCreditsError,
1687
1886
  KugelAudio,
1688
1887
  KugelAudioError,
1888
+ NotFoundError,
1689
1889
  RateLimitError,
1690
1890
  ValidationError,
1691
1891
  WsCloseCodes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kugelaudio",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Official JavaScript/TypeScript SDK for KugelAudio TTS API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -58,4 +58,4 @@
58
58
  "tsx": "^4.21.0",
59
59
  "ws": "^8.18.0"
60
60
  }
61
- }
61
+ }
@@ -180,42 +180,42 @@ describe('TTSResource.toReadable()', () => {
180
180
  // ---------------------------------------------------------------------------
181
181
 
182
182
  describe('KugelAudio multi-region', () => {
183
- it('defaults to EU when no region or prefix is given', () => {
183
+ it('uses canonical API when no region or prefix is given', () => {
184
184
  const client = new KugelAudio({ apiKey: 'ka_test123' });
185
185
  expect((client as any)._apiUrl).toBe('https://api.kugelaudio.com');
186
186
  });
187
187
 
188
- it('selects US endpoint with explicit region', () => {
188
+ it('selects canonical endpoint with explicit region', () => {
189
189
  const client = new KugelAudio({ apiKey: 'ka_test123', region: 'us' });
190
- expect((client as any)._apiUrl).toBe('https://us-api.kugelaudio.com');
190
+ expect((client as any)._apiUrl).toBe('https://api.kugelaudio.com');
191
191
  });
192
192
 
193
193
  it('selects global endpoint with explicit region', () => {
194
194
  const client = new KugelAudio({ apiKey: 'ka_test123', region: 'global' });
195
- expect((client as any)._apiUrl).toBe('https://global-api.kugelaudio.com');
195
+ expect((client as any)._apiUrl).toBe('https://api.kugelaudio.com');
196
196
  });
197
197
 
198
- it('detects US region from key prefix and strips it', () => {
198
+ it('detects canonical route from key prefix and strips it', () => {
199
199
  const client = new KugelAudio({ apiKey: 'us-ka_test123' });
200
- expect((client as any)._apiUrl).toBe('https://us-api.kugelaudio.com');
200
+ expect((client as any)._apiUrl).toBe('https://api.kugelaudio.com');
201
201
  expect((client as any)._apiKey).toBe('ka_test123');
202
202
  });
203
203
 
204
204
  it('detects global region from key prefix and strips it', () => {
205
205
  const client = new KugelAudio({ apiKey: 'global-ka_test123' });
206
- expect((client as any)._apiUrl).toBe('https://global-api.kugelaudio.com');
206
+ expect((client as any)._apiUrl).toBe('https://api.kugelaudio.com');
207
207
  expect((client as any)._apiKey).toBe('ka_test123');
208
208
  });
209
209
 
210
210
  it('detects EU region from key prefix and strips it', () => {
211
211
  const client = new KugelAudio({ apiKey: 'eu-ka_test123' });
212
- expect((client as any)._apiUrl).toBe('https://api.kugelaudio.com');
212
+ expect((client as any)._apiUrl).toBe('https://api.eu.kugelaudio.com');
213
213
  expect((client as any)._apiKey).toBe('ka_test123');
214
214
  });
215
215
 
216
216
  it('explicit region overrides key prefix', () => {
217
217
  const client = new KugelAudio({ apiKey: 'us-ka_test123', region: 'global' });
218
- expect((client as any)._apiUrl).toBe('https://global-api.kugelaudio.com');
218
+ expect((client as any)._apiUrl).toBe('https://api.kugelaudio.com');
219
219
  expect((client as any)._apiKey).toBe('ka_test123');
220
220
  });
221
221
 
@@ -237,7 +237,7 @@ describe('KugelAudio multi-region', () => {
237
237
 
238
238
  it('ttsUrl defaults to the resolved region URL', () => {
239
239
  const client = new KugelAudio({ apiKey: 'us-ka_test123' });
240
- expect((client as any)._ttsUrl).toBe('https://us-api.kugelaudio.com');
240
+ expect((client as any)._ttsUrl).toBe('https://api.kugelaudio.com');
241
241
  });
242
242
 
243
243
  it('unprefixed key is not modified', () => {
package/src/client.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * KugelAudio API Client.
3
3
  */
4
4
 
5
+ import { DictionariesResource } from './dictionaries';
5
6
  import {
6
7
  ConnectionError,
7
8
  KugelAudioError,
@@ -33,11 +34,9 @@ import { getWebSocket } from './websocket';
33
34
 
34
35
  import type { Region } from './types';
35
36
 
36
- const REGION_URLS: Record<Region, string> = {
37
- eu: 'https://api.kugelaudio.com',
38
- us: 'https://us-api.kugelaudio.com',
39
- global: 'https://global-api.kugelaudio.com',
40
- };
37
+ const DEFAULT_API_URL = 'https://api.kugelaudio.com';
38
+ const EU_API_URL = 'https://api.eu.kugelaudio.com';
39
+ const SUPPORTED_REGIONS = ['eu', 'us', 'global'] as const;
41
40
 
42
41
  const REGION_PREFIXES = ['eu-', 'us-', 'global-'] as const;
43
42
 
@@ -1616,6 +1615,8 @@ export class KugelAudio {
1616
1615
  public readonly models: ModelsResource;
1617
1616
  /** Voices resource */
1618
1617
  public readonly voices: VoicesResource;
1618
+ /** Custom dictionaries resource */
1619
+ public readonly dictionaries: DictionariesResource;
1619
1620
  /** TTS resource */
1620
1621
  public readonly tts: TTSResource;
1621
1622
 
@@ -1637,13 +1638,16 @@ export class KugelAudio {
1637
1638
  if (options.apiUrl) {
1638
1639
  this._apiUrl = options.apiUrl.replace(/\/$/, '');
1639
1640
  } else {
1640
- const effectiveRegion = options.region || detectedRegion || 'eu';
1641
- if (!(effectiveRegion in REGION_URLS)) {
1641
+ const effectiveRegion = options.region || detectedRegion;
1642
+ if (!effectiveRegion) {
1643
+ this._apiUrl = DEFAULT_API_URL;
1644
+ } else if (!SUPPORTED_REGIONS.includes(effectiveRegion as Region)) {
1642
1645
  throw new ValidationError(
1643
- `Invalid region '${effectiveRegion}'. Must be one of: ${Object.keys(REGION_URLS).join(', ')}.`,
1646
+ `Invalid region '${effectiveRegion}'. Must be one of: ${SUPPORTED_REGIONS.join(', ')}.`,
1644
1647
  );
1648
+ } else {
1649
+ this._apiUrl = effectiveRegion === 'eu' ? EU_API_URL : DEFAULT_API_URL;
1645
1650
  }
1646
- this._apiUrl = REGION_URLS[effectiveRegion as Region];
1647
1651
  }
1648
1652
 
1649
1653
  // If ttsUrl not specified, use apiUrl (backend proxies to TTS server)
@@ -1655,6 +1659,7 @@ export class KugelAudio {
1655
1659
 
1656
1660
  this.models = new ModelsResource(this);
1657
1661
  this.voices = new VoicesResource(this);
1662
+ this.dictionaries = new DictionariesResource(this);
1658
1663
  this.tts = new TTSResource(this);
1659
1664
  }
1660
1665
 
@@ -1843,4 +1848,3 @@ export class KugelAudio {
1843
1848
  }
1844
1849
  }
1845
1850
  }
1846
-