@modelrelay/sdk 0.2.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,960 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ApiKeysClient: () => ApiKeysClient,
24
+ AuthClient: () => AuthClient,
25
+ BillingClient: () => BillingClient,
26
+ ChatClient: () => ChatClient,
27
+ ChatCompletionsStream: () => ChatCompletionsStream,
28
+ DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
29
+ DEFAULT_CLIENT_HEADER: () => DEFAULT_CLIENT_HEADER,
30
+ DEFAULT_REQUEST_TIMEOUT_MS: () => DEFAULT_REQUEST_TIMEOUT_MS,
31
+ ModelRelay: () => ModelRelay,
32
+ ModelRelayError: () => ModelRelayError,
33
+ SANDBOX_BASE_URL: () => SANDBOX_BASE_URL,
34
+ SDK_VERSION: () => SDK_VERSION,
35
+ STAGING_BASE_URL: () => STAGING_BASE_URL,
36
+ isPublishableKey: () => isPublishableKey
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+
40
+ // src/errors.ts
41
+ var ModelRelayError = class extends Error {
42
+ constructor(message, opts) {
43
+ super(message);
44
+ this.name = "ModelRelayError";
45
+ this.status = opts.status;
46
+ this.code = opts.code;
47
+ this.requestId = opts.requestId;
48
+ this.fields = opts.fields;
49
+ this.data = opts.data;
50
+ }
51
+ };
52
+ async function parseErrorResponse(response) {
53
+ const requestId = response.headers.get("X-ModelRelay-Chat-Request-Id") || response.headers.get("X-Request-Id") || void 0;
54
+ const fallbackMessage = response.statusText || "Request failed";
55
+ const status = response.status || 500;
56
+ let bodyText = "";
57
+ try {
58
+ bodyText = await response.text();
59
+ } catch {
60
+ }
61
+ if (!bodyText) {
62
+ return new ModelRelayError(fallbackMessage, { status, requestId });
63
+ }
64
+ try {
65
+ const parsed = JSON.parse(bodyText);
66
+ const parsedObj = typeof parsed === "object" && parsed !== null ? parsed : null;
67
+ if (parsedObj?.error) {
68
+ const errPayload = typeof parsedObj.error === "object" && parsedObj.error !== null ? parsedObj.error : null;
69
+ const message = errPayload?.message || fallbackMessage;
70
+ const code = errPayload?.code || void 0;
71
+ const fields = Array.isArray(errPayload?.fields) ? errPayload?.fields : void 0;
72
+ const parsedStatus = typeof errPayload?.status === "number" ? errPayload.status : status;
73
+ return new ModelRelayError(message, {
74
+ status: parsedStatus,
75
+ code,
76
+ fields,
77
+ requestId: parsedObj?.request_id || parsedObj?.requestId || requestId,
78
+ data: parsed
79
+ });
80
+ }
81
+ if (parsedObj?.message || parsedObj?.code) {
82
+ const message = parsedObj.message || fallbackMessage;
83
+ return new ModelRelayError(message, {
84
+ status,
85
+ code: parsedObj.code,
86
+ fields: parsedObj.fields,
87
+ requestId: parsedObj?.request_id || parsedObj?.requestId || requestId,
88
+ data: parsed
89
+ });
90
+ }
91
+ return new ModelRelayError(fallbackMessage, {
92
+ status,
93
+ requestId,
94
+ data: parsed
95
+ });
96
+ } catch {
97
+ return new ModelRelayError(bodyText, { status, requestId });
98
+ }
99
+ }
100
+
101
+ // src/auth.ts
102
+ var AuthClient = class {
103
+ constructor(http, cfg) {
104
+ this.cachedFrontend = /* @__PURE__ */ new Map();
105
+ this.http = http;
106
+ this.apiKey = cfg.apiKey;
107
+ this.accessToken = cfg.accessToken;
108
+ this.endUser = cfg.endUser;
109
+ }
110
+ /**
111
+ * Exchange a publishable key for a short-lived frontend token.
112
+ * Tokens are cached until they are close to expiry.
113
+ */
114
+ async frontendToken(request) {
115
+ const publishableKey = request?.publishableKey || (isPublishableKey(this.apiKey) ? this.apiKey : void 0);
116
+ if (!publishableKey) {
117
+ throw new ModelRelayError(
118
+ "publishable key required to issue frontend tokens",
119
+ { status: 400 }
120
+ );
121
+ }
122
+ const userId = request?.userId || this.endUser?.id;
123
+ if (!userId) {
124
+ throw new ModelRelayError(
125
+ "endUserId is required to mint a frontend token",
126
+ { status: 400 }
127
+ );
128
+ }
129
+ const deviceId = request?.deviceId || this.endUser?.deviceId;
130
+ const ttlSeconds = request?.ttlSeconds ?? this.endUser?.ttlSeconds;
131
+ const cacheKey = `${publishableKey}:${userId}:${deviceId || ""}`;
132
+ const cached = this.cachedFrontend.get(cacheKey);
133
+ if (cached && isTokenReusable(cached)) {
134
+ return cached;
135
+ }
136
+ const payload = {
137
+ publishable_key: publishableKey,
138
+ user_id: userId
139
+ };
140
+ if (deviceId) {
141
+ payload.device_id = deviceId;
142
+ }
143
+ if (typeof ttlSeconds === "number" && ttlSeconds > 0) {
144
+ payload.ttl_seconds = ttlSeconds;
145
+ }
146
+ const response = await this.http.json(
147
+ "/auth/frontend-token",
148
+ {
149
+ method: "POST",
150
+ body: payload
151
+ }
152
+ );
153
+ const token = normalizeFrontendToken(response, {
154
+ publishableKey,
155
+ userId,
156
+ deviceId
157
+ });
158
+ this.cachedFrontend.set(cacheKey, token);
159
+ return token;
160
+ }
161
+ /**
162
+ * Determine the correct auth headers for chat completions.
163
+ * Publishable keys are automatically exchanged for frontend tokens.
164
+ */
165
+ async authForChat(endUserId, overrides) {
166
+ if (this.accessToken) {
167
+ return { accessToken: this.accessToken };
168
+ }
169
+ if (!this.apiKey) {
170
+ throw new ModelRelayError("API key or token is required", {
171
+ status: 401
172
+ });
173
+ }
174
+ if (isPublishableKey(this.apiKey)) {
175
+ const token = await this.frontendToken({
176
+ userId: endUserId || overrides?.id,
177
+ deviceId: overrides?.deviceId,
178
+ ttlSeconds: overrides?.ttlSeconds
179
+ });
180
+ return { accessToken: token.token };
181
+ }
182
+ return { apiKey: this.apiKey };
183
+ }
184
+ /**
185
+ * Billing calls accept either bearer tokens or API keys (including publishable keys).
186
+ */
187
+ authForBilling() {
188
+ if (this.accessToken) {
189
+ return { accessToken: this.accessToken };
190
+ }
191
+ if (!this.apiKey) {
192
+ throw new ModelRelayError("API key or token is required", {
193
+ status: 401
194
+ });
195
+ }
196
+ return { apiKey: this.apiKey };
197
+ }
198
+ };
199
+ function isPublishableKey(value) {
200
+ if (!value) {
201
+ return false;
202
+ }
203
+ return value.trim().toLowerCase().startsWith("mr_pk_");
204
+ }
205
+ function normalizeFrontendToken(payload, meta) {
206
+ const expiresAt = payload.expires_at || payload.expiresAt;
207
+ return {
208
+ token: payload.token,
209
+ expiresAt: expiresAt ? new Date(expiresAt) : void 0,
210
+ expiresIn: payload.expires_in ?? payload.expiresIn,
211
+ tokenType: payload.token_type ?? payload.tokenType,
212
+ keyId: payload.key_id ?? payload.keyId,
213
+ sessionId: payload.session_id ?? payload.sessionId,
214
+ tokenScope: payload.token_scope ?? payload.tokenScope,
215
+ tokenSource: payload.token_source ?? payload.tokenSource,
216
+ endUserId: meta.userId,
217
+ publishableKey: meta.publishableKey,
218
+ deviceId: meta.deviceId
219
+ };
220
+ }
221
+ function isTokenReusable(token) {
222
+ if (!token.token) {
223
+ return false;
224
+ }
225
+ if (!token.expiresAt) {
226
+ return true;
227
+ }
228
+ return token.expiresAt.getTime() - Date.now() > 6e4;
229
+ }
230
+
231
+ // src/api-keys.ts
232
+ var ApiKeysClient = class {
233
+ constructor(http) {
234
+ this.http = http;
235
+ }
236
+ async list() {
237
+ const payload = await this.http.json("/api-keys", {
238
+ method: "GET"
239
+ });
240
+ const items = payload.api_keys || payload.apiKeys || [];
241
+ return items.map(normalizeApiKey).filter(Boolean);
242
+ }
243
+ async create(req) {
244
+ if (!req?.label?.trim()) {
245
+ throw new ModelRelayError("label is required", { status: 400 });
246
+ }
247
+ const body = {
248
+ label: req.label
249
+ };
250
+ if (req.kind) body.kind = req.kind;
251
+ if (req.expiresAt instanceof Date) {
252
+ body.expires_at = req.expiresAt.toISOString();
253
+ }
254
+ const payload = await this.http.json("/api-keys", {
255
+ method: "POST",
256
+ body
257
+ });
258
+ const record = payload.api_key || payload.apiKey;
259
+ if (!record) {
260
+ throw new ModelRelayError("missing api_key in response", {
261
+ status: 500
262
+ });
263
+ }
264
+ return normalizeApiKey(record);
265
+ }
266
+ async delete(id) {
267
+ if (!id?.trim()) {
268
+ throw new ModelRelayError("id is required", { status: 400 });
269
+ }
270
+ await this.http.request(`/api-keys/${encodeURIComponent(id)}`, {
271
+ method: "DELETE"
272
+ });
273
+ }
274
+ };
275
+ function normalizeApiKey(record) {
276
+ const created = record?.created_at || record?.createdAt || "";
277
+ const expires = record?.expires_at ?? record?.expiresAt ?? void 0;
278
+ const lastUsed = record?.last_used_at ?? record?.lastUsedAt ?? void 0;
279
+ return {
280
+ id: record?.id || "",
281
+ label: record?.label || "",
282
+ kind: record?.kind || "",
283
+ createdAt: created ? new Date(created) : /* @__PURE__ */ new Date(),
284
+ expiresAt: expires ? new Date(expires) : void 0,
285
+ lastUsedAt: lastUsed ? new Date(lastUsed) : void 0,
286
+ redactedKey: record?.redacted_key || record?.redactedKey || "",
287
+ secretKey: record?.secret_key ?? record?.secretKey ?? void 0
288
+ };
289
+ }
290
+
291
+ // src/billing.ts
292
+ var BillingClient = class {
293
+ constructor(http, auth) {
294
+ this.http = http;
295
+ this.auth = auth;
296
+ }
297
+ /**
298
+ * Initiate a Stripe Checkout session for an end user.
299
+ */
300
+ async checkout(params) {
301
+ if (!params?.endUserId?.trim()) {
302
+ throw new ModelRelayError("endUserId is required", { status: 400 });
303
+ }
304
+ if (!params.successUrl?.trim() || !params.cancelUrl?.trim()) {
305
+ throw new ModelRelayError("successUrl and cancelUrl are required", {
306
+ status: 400
307
+ });
308
+ }
309
+ const authHeaders = this.auth.authForBilling();
310
+ const body = {
311
+ end_user_id: params.endUserId,
312
+ success_url: params.successUrl,
313
+ cancel_url: params.cancelUrl
314
+ };
315
+ if (params.deviceId) body.device_id = params.deviceId;
316
+ if (params.planId) body.plan_id = params.planId;
317
+ if (params.plan) body.plan = params.plan;
318
+ const response = await this.http.json(
319
+ "/end-users/checkout",
320
+ {
321
+ method: "POST",
322
+ body,
323
+ apiKey: authHeaders.apiKey,
324
+ accessToken: authHeaders.accessToken
325
+ }
326
+ );
327
+ return normalizeCheckoutResponse(response);
328
+ }
329
+ };
330
+ function normalizeCheckoutResponse(payload) {
331
+ const endUser = {
332
+ id: payload.end_user?.id || "",
333
+ externalId: payload.end_user?.external_id || "",
334
+ ownerId: payload.end_user?.owner_id || ""
335
+ };
336
+ const session = {
337
+ id: payload.session?.id || "",
338
+ plan: payload.session?.plan || "",
339
+ status: payload.session?.status || "",
340
+ url: payload.session?.url || "",
341
+ expiresAt: payload.session?.expires_at ? new Date(payload.session.expires_at) : void 0,
342
+ completedAt: payload.session?.completed_at ? new Date(payload.session.completed_at) : void 0
343
+ };
344
+ return { endUser, session };
345
+ }
346
+
347
+ // src/chat.ts
348
+ var REQUEST_ID_HEADER = "X-ModelRelay-Chat-Request-Id";
349
+ var ChatClient = class {
350
+ constructor(http, auth, cfg = {}) {
351
+ this.completions = new ChatCompletionsClient(
352
+ http,
353
+ auth,
354
+ cfg.defaultMetadata
355
+ );
356
+ }
357
+ };
358
+ var ChatCompletionsClient = class {
359
+ constructor(http, auth, defaultMetadata) {
360
+ this.http = http;
361
+ this.auth = auth;
362
+ this.defaultMetadata = defaultMetadata;
363
+ }
364
+ async create(params, options = {}) {
365
+ const stream = options.stream ?? params.stream ?? true;
366
+ if (!params?.model?.trim()) {
367
+ throw new ModelRelayError("model is required", { status: 400 });
368
+ }
369
+ if (!params?.messages?.length) {
370
+ throw new ModelRelayError("at least one message is required", {
371
+ status: 400
372
+ });
373
+ }
374
+ if (!hasUserMessage(params.messages)) {
375
+ throw new ModelRelayError(
376
+ "at least one user message is required",
377
+ { status: 400 }
378
+ );
379
+ }
380
+ const authHeaders = await this.auth.authForChat(params.endUserId);
381
+ const body = buildProxyBody(
382
+ params,
383
+ mergeMetadata(this.defaultMetadata, params.metadata, options.metadata)
384
+ );
385
+ const requestId = params.requestId || options.requestId;
386
+ const headers = { ...options.headers || {} };
387
+ if (requestId) {
388
+ headers[REQUEST_ID_HEADER] = requestId;
389
+ }
390
+ const response = await this.http.request("/llm/proxy", {
391
+ method: "POST",
392
+ body,
393
+ headers,
394
+ apiKey: authHeaders.apiKey,
395
+ accessToken: authHeaders.accessToken,
396
+ accept: stream ? "text/event-stream" : "application/json",
397
+ raw: true,
398
+ signal: options.signal,
399
+ timeoutMs: options.timeoutMs ?? (stream ? 0 : void 0),
400
+ useDefaultTimeout: !stream,
401
+ retry: options.retry
402
+ });
403
+ const resolvedRequestId = requestIdFromHeaders(response.headers) || requestId || void 0;
404
+ if (!response.ok) {
405
+ throw await parseErrorResponse(response);
406
+ }
407
+ if (!stream) {
408
+ const payload = await response.json();
409
+ return normalizeChatResponse(payload, resolvedRequestId);
410
+ }
411
+ return new ChatCompletionsStream(response, resolvedRequestId);
412
+ }
413
+ };
414
+ var ChatCompletionsStream = class {
415
+ constructor(response, requestId) {
416
+ this.closed = false;
417
+ if (!response.body) {
418
+ throw new ModelRelayError("streaming response is missing a body", {
419
+ status: 500
420
+ });
421
+ }
422
+ this.response = response;
423
+ this.requestId = requestId;
424
+ }
425
+ async cancel(reason) {
426
+ this.closed = true;
427
+ try {
428
+ await this.response.body?.cancel(reason);
429
+ } catch {
430
+ }
431
+ }
432
+ async *[Symbol.asyncIterator]() {
433
+ if (this.closed) {
434
+ return;
435
+ }
436
+ const body = this.response.body;
437
+ if (!body) {
438
+ throw new ModelRelayError("streaming response is missing a body", {
439
+ status: 500
440
+ });
441
+ }
442
+ const reader = body.getReader();
443
+ const decoder = new TextDecoder();
444
+ let buffer = "";
445
+ try {
446
+ while (true) {
447
+ if (this.closed) {
448
+ await reader.cancel();
449
+ return;
450
+ }
451
+ const { value, done } = await reader.read();
452
+ if (done) {
453
+ const { events: events2 } = consumeSSEBuffer(buffer, true);
454
+ for (const evt of events2) {
455
+ const parsed = mapChatEvent(evt, this.requestId);
456
+ if (parsed) {
457
+ yield parsed;
458
+ }
459
+ }
460
+ return;
461
+ }
462
+ buffer += decoder.decode(value, { stream: true });
463
+ const { events, remainder } = consumeSSEBuffer(buffer);
464
+ buffer = remainder;
465
+ for (const evt of events) {
466
+ const parsed = mapChatEvent(evt, this.requestId);
467
+ if (parsed) {
468
+ yield parsed;
469
+ }
470
+ }
471
+ }
472
+ } finally {
473
+ this.closed = true;
474
+ reader.releaseLock();
475
+ }
476
+ }
477
+ };
478
+ function consumeSSEBuffer(buffer, flush = false) {
479
+ const events = [];
480
+ let eventName = "";
481
+ let dataLines = [];
482
+ let remainder = "";
483
+ const lines = buffer.split(/\r?\n/);
484
+ const lastIndex = lines.length - 1;
485
+ const limit = flush ? lines.length : Math.max(0, lastIndex);
486
+ const pushEvent = () => {
487
+ if (!eventName && dataLines.length === 0) {
488
+ return;
489
+ }
490
+ events.push({
491
+ event: eventName || "message",
492
+ data: dataLines.join("\n")
493
+ });
494
+ eventName = "";
495
+ dataLines = [];
496
+ };
497
+ for (let i = 0; i < limit; i++) {
498
+ const line = lines[i];
499
+ if (line === "") {
500
+ pushEvent();
501
+ continue;
502
+ }
503
+ if (line.startsWith(":")) {
504
+ continue;
505
+ }
506
+ if (line.startsWith("event:")) {
507
+ eventName = line.slice(6).trim();
508
+ } else if (line.startsWith("data:")) {
509
+ dataLines.push(line.slice(5).trimStart());
510
+ }
511
+ }
512
+ if (flush) {
513
+ pushEvent();
514
+ remainder = "";
515
+ } else {
516
+ remainder = lines[lastIndex] ?? "";
517
+ }
518
+ return { events, remainder };
519
+ }
520
+ function mapChatEvent(raw, requestId) {
521
+ let parsed = raw.data;
522
+ if (raw.data) {
523
+ try {
524
+ parsed = JSON.parse(raw.data);
525
+ } catch {
526
+ parsed = raw.data;
527
+ }
528
+ }
529
+ const payload = typeof parsed === "object" && parsed !== null ? parsed : {};
530
+ const p = payload;
531
+ const type = normalizeEventType(raw.event, p);
532
+ const usage = normalizeUsage(p.usage);
533
+ const responseId = p.response_id || p.responseId || p.id || p?.message?.id;
534
+ const model = p.model || p?.message?.model;
535
+ const stopReason = p.stop_reason || p.stopReason;
536
+ const textDelta = extractTextDelta(p);
537
+ return {
538
+ type,
539
+ event: raw.event || type,
540
+ data: p,
541
+ textDelta,
542
+ responseId,
543
+ model,
544
+ stopReason,
545
+ usage,
546
+ requestId,
547
+ raw: raw.data || ""
548
+ };
549
+ }
550
+ function normalizeEventType(eventName, payload) {
551
+ const hint = String(
552
+ payload?.type || payload?.event || eventName || ""
553
+ ).trim();
554
+ switch (hint) {
555
+ case "message_start":
556
+ return "message_start";
557
+ case "message_delta":
558
+ return "message_delta";
559
+ case "message_stop":
560
+ return "message_stop";
561
+ case "ping":
562
+ return "ping";
563
+ default:
564
+ return "custom";
565
+ }
566
+ }
567
+ function extractTextDelta(payload) {
568
+ if (!payload || typeof payload !== "object") {
569
+ return void 0;
570
+ }
571
+ if (typeof payload.delta === "string") {
572
+ return payload.delta;
573
+ }
574
+ if (payload.delta && typeof payload.delta === "object") {
575
+ if (typeof payload.delta.text === "string") {
576
+ return payload.delta.text;
577
+ }
578
+ if (typeof payload.delta.content === "string") {
579
+ return payload.delta.content;
580
+ }
581
+ }
582
+ return void 0;
583
+ }
584
+ function normalizeChatResponse(payload, requestId) {
585
+ const p = payload;
586
+ return {
587
+ id: p?.id,
588
+ provider: p?.provider,
589
+ content: Array.isArray(p?.content) ? p.content : p?.content ? [String(p.content)] : [],
590
+ stopReason: p?.stop_reason ?? p?.stopReason,
591
+ model: p?.model,
592
+ usage: normalizeUsage(p?.usage),
593
+ requestId
594
+ };
595
+ }
596
+ function normalizeUsage(payload) {
597
+ if (!payload) {
598
+ return { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
599
+ }
600
+ return {
601
+ inputTokens: Number(payload.input_tokens ?? payload.inputTokens ?? 0),
602
+ outputTokens: Number(payload.output_tokens ?? payload.outputTokens ?? 0),
603
+ totalTokens: Number(payload.total_tokens ?? payload.totalTokens ?? 0)
604
+ };
605
+ }
606
+ function buildProxyBody(params, metadata) {
607
+ const body = {
608
+ model: params.model,
609
+ messages: normalizeMessages(params.messages)
610
+ };
611
+ if (typeof params.maxTokens === "number") body.max_tokens = params.maxTokens;
612
+ if (params.provider) body.provider = params.provider;
613
+ if (typeof params.temperature === "number")
614
+ body.temperature = params.temperature;
615
+ if (metadata && Object.keys(metadata).length > 0) body.metadata = metadata;
616
+ if (params.stop?.length) body.stop = params.stop;
617
+ if (params.stopSequences?.length) body.stop_sequences = params.stopSequences;
618
+ return body;
619
+ }
620
+ function normalizeMessages(messages) {
621
+ return messages.map((msg) => ({
622
+ role: msg.role || "user",
623
+ content: msg.content
624
+ }));
625
+ }
626
+ function requestIdFromHeaders(headers) {
627
+ return headers.get(REQUEST_ID_HEADER) || headers.get("X-Request-Id") || void 0;
628
+ }
629
+ function mergeMetadata(...sources) {
630
+ const merged = {};
631
+ for (const src of sources) {
632
+ if (!src) continue;
633
+ for (const [key, value] of Object.entries(src)) {
634
+ const k = key?.trim();
635
+ const v = value?.trim();
636
+ if (!k || !v) continue;
637
+ merged[k] = v;
638
+ }
639
+ }
640
+ return Object.keys(merged).length ? merged : void 0;
641
+ }
642
+ function hasUserMessage(messages) {
643
+ return messages.some(
644
+ (msg) => msg.role?.toLowerCase?.() === "user" && !!msg.content
645
+ );
646
+ }
647
+
648
+ // package.json
649
+ var package_default = {
650
+ name: "@modelrelay/sdk",
651
+ version: "0.2.0",
652
+ description: "TypeScript SDK for the ModelRelay API",
653
+ type: "module",
654
+ main: "dist/index.cjs",
655
+ module: "dist/index.js",
656
+ types: "dist/index.d.ts",
657
+ exports: {
658
+ ".": {
659
+ types: "./dist/index.d.ts",
660
+ import: "./dist/index.js",
661
+ require: "./dist/index.cjs"
662
+ }
663
+ },
664
+ publishConfig: { access: "public" },
665
+ files: [
666
+ "dist"
667
+ ],
668
+ scripts: {
669
+ build: "tsup src/index.ts --format esm,cjs --dts",
670
+ dev: "tsup src/index.ts --format esm,cjs --dts --watch",
671
+ lint: "tsc --noEmit",
672
+ test: "vitest run"
673
+ },
674
+ keywords: [
675
+ "modelrelay",
676
+ "llm",
677
+ "sdk",
678
+ "typescript"
679
+ ],
680
+ author: "Shane Vitarana",
681
+ license: "Apache-2.0",
682
+ devDependencies: {
683
+ tsup: "^8.2.4",
684
+ typescript: "^5.6.3",
685
+ vitest: "^2.1.4"
686
+ }
687
+ };
688
+
689
+ // src/types.ts
690
+ var SDK_VERSION = package_default.version || "0.0.0";
691
+ var DEFAULT_BASE_URL = "https://api.modelrelay.ai/api/v1";
692
+ var STAGING_BASE_URL = "https://api-stg.modelrelay.ai/api/v1";
693
+ var SANDBOX_BASE_URL = "https://api.sandbox.modelrelay.ai/api/v1";
694
+ var DEFAULT_CLIENT_HEADER = `modelrelay-ts/${SDK_VERSION}`;
695
+ var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
696
+
697
+ // src/http.ts
698
+ var HTTPClient = class {
699
+ constructor(cfg) {
700
+ const baseFromEnv = baseUrlForEnvironment(cfg.environment);
701
+ this.baseUrl = normalizeBaseUrl(cfg.baseUrl || baseFromEnv || DEFAULT_BASE_URL);
702
+ this.apiKey = cfg.apiKey?.trim();
703
+ this.accessToken = cfg.accessToken?.trim();
704
+ this.fetchImpl = cfg.fetchImpl;
705
+ this.clientHeader = cfg.clientHeader?.trim() || DEFAULT_CLIENT_HEADER;
706
+ this.defaultTimeoutMs = cfg.timeoutMs === void 0 ? DEFAULT_REQUEST_TIMEOUT_MS : Math.max(0, cfg.timeoutMs);
707
+ this.retry = normalizeRetryConfig(cfg.retry);
708
+ this.defaultHeaders = normalizeHeaders(cfg.defaultHeaders);
709
+ }
710
+ async request(path, options = {}) {
711
+ const fetchFn = this.fetchImpl ?? globalThis.fetch;
712
+ if (!fetchFn) {
713
+ throw new ModelRelayError(
714
+ "fetch is not available; provide a fetch implementation",
715
+ { status: 500 }
716
+ );
717
+ }
718
+ const method = options.method || "GET";
719
+ const url = buildUrl(this.baseUrl, path);
720
+ const headers = new Headers({
721
+ ...this.defaultHeaders,
722
+ ...options.headers || {}
723
+ });
724
+ const accepts = options.accept || (options.raw ? void 0 : "application/json");
725
+ if (accepts && !headers.has("Accept")) {
726
+ headers.set("Accept", accepts);
727
+ }
728
+ const body = options.body;
729
+ const shouldEncodeJSON = body !== void 0 && body !== null && typeof body === "object" && !(body instanceof FormData) && !(body instanceof Blob);
730
+ const payload = shouldEncodeJSON ? JSON.stringify(body) : body;
731
+ if (shouldEncodeJSON && !headers.has("Content-Type")) {
732
+ headers.set("Content-Type", "application/json");
733
+ }
734
+ const accessToken = options.accessToken ?? this.accessToken;
735
+ if (accessToken) {
736
+ const bearer = accessToken.toLowerCase().startsWith("bearer ") ? accessToken : `Bearer ${accessToken}`;
737
+ headers.set("Authorization", bearer);
738
+ }
739
+ const apiKey = options.apiKey ?? this.apiKey;
740
+ if (apiKey) {
741
+ headers.set("X-ModelRelay-Api-Key", apiKey);
742
+ }
743
+ if (this.clientHeader && !headers.has("X-ModelRelay-Client")) {
744
+ headers.set("X-ModelRelay-Client", this.clientHeader);
745
+ }
746
+ const timeoutMs = options.useDefaultTimeout === false ? options.timeoutMs : options.timeoutMs ?? this.defaultTimeoutMs;
747
+ const retryCfg = normalizeRetryConfig(
748
+ options.retry === void 0 ? this.retry : options.retry
749
+ );
750
+ const attempts = retryCfg ? Math.max(1, retryCfg.maxAttempts) : 1;
751
+ let lastError;
752
+ for (let attempt = 1; attempt <= attempts; attempt++) {
753
+ const controller = timeoutMs && timeoutMs > 0 ? new AbortController() : void 0;
754
+ const signal = mergeSignals(options.signal, controller?.signal);
755
+ const timer = controller && setTimeout(() => controller.abort(new DOMException("timeout", "AbortError")), timeoutMs);
756
+ try {
757
+ const response = await fetchFn(url, {
758
+ method,
759
+ headers,
760
+ body: payload,
761
+ signal
762
+ });
763
+ if (!response.ok) {
764
+ const shouldRetry = retryCfg && shouldRetryStatus(
765
+ response.status,
766
+ method,
767
+ retryCfg.retryPost
768
+ ) && attempt < attempts;
769
+ if (shouldRetry) {
770
+ await backoff(attempt, retryCfg);
771
+ continue;
772
+ }
773
+ if (!options.raw) {
774
+ throw await parseErrorResponse(response);
775
+ }
776
+ }
777
+ return response;
778
+ } catch (err) {
779
+ if (options.signal?.aborted) {
780
+ throw err;
781
+ }
782
+ const shouldRetry = retryCfg && isRetryableError(err) && (method !== "POST" || retryCfg.retryPost) && attempt < attempts;
783
+ if (!shouldRetry) {
784
+ throw err;
785
+ }
786
+ lastError = err;
787
+ await backoff(attempt, retryCfg);
788
+ } finally {
789
+ if (timer) {
790
+ clearTimeout(timer);
791
+ }
792
+ }
793
+ }
794
+ throw lastError instanceof Error ? lastError : new ModelRelayError("request failed", { status: 500 });
795
+ }
796
+ async json(path, options = {}) {
797
+ const response = await this.request(path, {
798
+ ...options,
799
+ raw: true,
800
+ accept: options.accept || "application/json"
801
+ });
802
+ if (!response.ok) {
803
+ throw await parseErrorResponse(response);
804
+ }
805
+ if (response.status === 204) {
806
+ return void 0;
807
+ }
808
+ try {
809
+ return await response.json();
810
+ } catch (err) {
811
+ throw new ModelRelayError("failed to parse response JSON", {
812
+ status: response.status,
813
+ data: err
814
+ });
815
+ }
816
+ }
817
+ };
818
+ function buildUrl(baseUrl, path) {
819
+ if (/^https?:\/\//i.test(path)) {
820
+ return path;
821
+ }
822
+ if (!path.startsWith("/")) {
823
+ path = `/${path}`;
824
+ }
825
+ return `${baseUrl}${path}`;
826
+ }
827
+ function normalizeBaseUrl(value) {
828
+ const trimmed = value.trim();
829
+ if (trimmed.endsWith("/")) {
830
+ return trimmed.slice(0, -1);
831
+ }
832
+ return trimmed;
833
+ }
834
+ function baseUrlForEnvironment(env) {
835
+ if (!env || env === "production") return void 0;
836
+ if (env === "staging") return STAGING_BASE_URL;
837
+ if (env === "sandbox") return SANDBOX_BASE_URL;
838
+ return void 0;
839
+ }
840
+ function normalizeRetryConfig(retry) {
841
+ if (retry === false) return void 0;
842
+ const cfg = retry || {};
843
+ return {
844
+ maxAttempts: Math.max(1, cfg.maxAttempts ?? 3),
845
+ baseBackoffMs: Math.max(0, cfg.baseBackoffMs ?? 300),
846
+ maxBackoffMs: Math.max(0, cfg.maxBackoffMs ?? 5e3),
847
+ retryPost: cfg.retryPost ?? true
848
+ };
849
+ }
850
+ function shouldRetryStatus(status, method, retryPost) {
851
+ if (status === 408 || status === 429) {
852
+ return method !== "POST" || retryPost;
853
+ }
854
+ if (status >= 500 && status < 600) {
855
+ return method !== "POST" || retryPost;
856
+ }
857
+ return false;
858
+ }
859
+ function isRetryableError(err) {
860
+ if (!err) return false;
861
+ return err instanceof DOMException && err.name === "AbortError" || err instanceof TypeError;
862
+ }
863
+ function backoff(attempt, cfg) {
864
+ const exp = Math.max(0, attempt - 1);
865
+ const base = cfg.baseBackoffMs * Math.pow(2, Math.min(exp, 10));
866
+ const capped = Math.min(base, cfg.maxBackoffMs);
867
+ const jitter = 0.5 + Math.random();
868
+ const delay = Math.min(cfg.maxBackoffMs, capped * jitter);
869
+ if (delay <= 0) return Promise.resolve();
870
+ return new Promise((resolve) => setTimeout(resolve, delay));
871
+ }
872
+ function mergeSignals(user, timeoutSignal) {
873
+ if (!user && !timeoutSignal) return void 0;
874
+ if (user && !timeoutSignal) return user;
875
+ if (!user && timeoutSignal) return timeoutSignal;
876
+ const controller = new AbortController();
877
+ const propagate = (source) => {
878
+ if (source.aborted) {
879
+ controller.abort(source.reason);
880
+ } else {
881
+ source.addEventListener(
882
+ "abort",
883
+ () => controller.abort(source.reason),
884
+ { once: true }
885
+ );
886
+ }
887
+ };
888
+ if (user) propagate(user);
889
+ if (timeoutSignal) propagate(timeoutSignal);
890
+ return controller.signal;
891
+ }
892
+ function normalizeHeaders(headers) {
893
+ if (!headers) return {};
894
+ const normalized = {};
895
+ for (const [key, value] of Object.entries(headers)) {
896
+ if (!key || !value) continue;
897
+ const k = key.trim();
898
+ const v = value.trim();
899
+ if (k && v) {
900
+ normalized[k] = v;
901
+ }
902
+ }
903
+ return normalized;
904
+ }
905
+
906
+ // src/index.ts
907
+ var ModelRelay = class {
908
+ constructor(options) {
909
+ const cfg = options || {};
910
+ if (!cfg.key && !cfg.token) {
911
+ throw new ModelRelayError("Provide an API key or access token", {
912
+ status: 400
913
+ });
914
+ }
915
+ this.baseUrl = resolveBaseUrl(cfg.environment, cfg.baseUrl);
916
+ const http = new HTTPClient({
917
+ baseUrl: this.baseUrl,
918
+ apiKey: cfg.key,
919
+ accessToken: cfg.token,
920
+ fetchImpl: cfg.fetch,
921
+ clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
922
+ timeoutMs: cfg.timeoutMs,
923
+ retry: cfg.retry,
924
+ defaultHeaders: cfg.defaultHeaders,
925
+ environment: cfg.environment
926
+ });
927
+ const auth = new AuthClient(http, {
928
+ apiKey: cfg.key,
929
+ accessToken: cfg.token,
930
+ endUser: cfg.endUser
931
+ });
932
+ this.auth = auth;
933
+ this.billing = new BillingClient(http, auth);
934
+ this.chat = new ChatClient(http, auth, {
935
+ defaultMetadata: cfg.defaultMetadata
936
+ });
937
+ this.apiKeys = new ApiKeysClient(http);
938
+ }
939
+ };
940
+ function resolveBaseUrl(env, override) {
941
+ const base = override || (env === "staging" ? STAGING_BASE_URL : env === "sandbox" ? SANDBOX_BASE_URL : DEFAULT_BASE_URL);
942
+ return base.replace(/\/+$/, "");
943
+ }
944
+ // Annotate the CommonJS export names for ESM import in node:
945
+ 0 && (module.exports = {
946
+ ApiKeysClient,
947
+ AuthClient,
948
+ BillingClient,
949
+ ChatClient,
950
+ ChatCompletionsStream,
951
+ DEFAULT_BASE_URL,
952
+ DEFAULT_CLIENT_HEADER,
953
+ DEFAULT_REQUEST_TIMEOUT_MS,
954
+ ModelRelay,
955
+ ModelRelayError,
956
+ SANDBOX_BASE_URL,
957
+ SDK_VERSION,
958
+ STAGING_BASE_URL,
959
+ isPublishableKey
960
+ });