@modelrelay/sdk 0.2.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.cjs CHANGED
@@ -20,20 +20,36 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- ApiKeysClient: () => ApiKeysClient,
23
+ APIError: () => APIError,
24
24
  AuthClient: () => AuthClient,
25
- BillingClient: () => BillingClient,
26
25
  ChatClient: () => ChatClient,
27
26
  ChatCompletionsStream: () => ChatCompletionsStream,
27
+ ConfigError: () => ConfigError,
28
+ CustomersClient: () => CustomersClient,
28
29
  DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
29
30
  DEFAULT_CLIENT_HEADER: () => DEFAULT_CLIENT_HEADER,
31
+ DEFAULT_CONNECT_TIMEOUT_MS: () => DEFAULT_CONNECT_TIMEOUT_MS,
30
32
  DEFAULT_REQUEST_TIMEOUT_MS: () => DEFAULT_REQUEST_TIMEOUT_MS,
31
33
  ModelRelay: () => ModelRelay,
32
34
  ModelRelayError: () => ModelRelayError,
35
+ Models: () => Models,
36
+ Providers: () => Providers,
33
37
  SANDBOX_BASE_URL: () => SANDBOX_BASE_URL,
34
38
  SDK_VERSION: () => SDK_VERSION,
35
39
  STAGING_BASE_URL: () => STAGING_BASE_URL,
36
- isPublishableKey: () => isPublishableKey
40
+ StopReasons: () => StopReasons,
41
+ TiersClient: () => TiersClient,
42
+ TransportError: () => TransportError,
43
+ isPublishableKey: () => isPublishableKey,
44
+ mergeMetrics: () => mergeMetrics,
45
+ mergeTrace: () => mergeTrace,
46
+ modelToString: () => modelToString,
47
+ normalizeModelId: () => normalizeModelId,
48
+ normalizeProvider: () => normalizeProvider,
49
+ normalizeStopReason: () => normalizeStopReason,
50
+ parseErrorResponse: () => parseErrorResponse,
51
+ providerToString: () => providerToString,
52
+ stopReasonToString: () => stopReasonToString
37
53
  });
38
54
  module.exports = __toCommonJS(index_exports);
39
55
 
@@ -41,15 +57,48 @@ module.exports = __toCommonJS(index_exports);
41
57
  var ModelRelayError = class extends Error {
42
58
  constructor(message, opts) {
43
59
  super(message);
44
- this.name = "ModelRelayError";
60
+ this.name = this.constructor.name;
61
+ this.category = opts.category;
45
62
  this.status = opts.status;
46
63
  this.code = opts.code;
47
64
  this.requestId = opts.requestId;
48
65
  this.fields = opts.fields;
49
66
  this.data = opts.data;
67
+ this.retries = opts.retries;
68
+ this.cause = opts.cause;
50
69
  }
51
70
  };
52
- async function parseErrorResponse(response) {
71
+ var ConfigError = class extends ModelRelayError {
72
+ constructor(message, data) {
73
+ super(message, { category: "config", status: 400, data });
74
+ }
75
+ };
76
+ var TransportError = class extends ModelRelayError {
77
+ constructor(message, opts) {
78
+ super(message, {
79
+ category: "transport",
80
+ status: opts.kind === "timeout" ? 408 : 0,
81
+ retries: opts.retries,
82
+ cause: opts.cause,
83
+ data: opts.cause
84
+ });
85
+ this.kind = opts.kind;
86
+ }
87
+ };
88
+ var APIError = class extends ModelRelayError {
89
+ constructor(message, opts) {
90
+ super(message, {
91
+ category: "api",
92
+ status: opts.status,
93
+ code: opts.code,
94
+ requestId: opts.requestId,
95
+ fields: opts.fields,
96
+ data: opts.data,
97
+ retries: opts.retries
98
+ });
99
+ }
100
+ };
101
+ async function parseErrorResponse(response, retries) {
53
102
  const requestId = response.headers.get("X-ModelRelay-Chat-Request-Id") || response.headers.get("X-Request-Id") || void 0;
54
103
  const fallbackMessage = response.statusText || "Request failed";
55
104
  const status = response.status || 500;
@@ -59,7 +108,7 @@ async function parseErrorResponse(response) {
59
108
  } catch {
60
109
  }
61
110
  if (!bodyText) {
62
- return new ModelRelayError(fallbackMessage, { status, requestId });
111
+ return new APIError(fallbackMessage, { status, requestId, retries });
63
112
  }
64
113
  try {
65
114
  const parsed = JSON.parse(bodyText);
@@ -70,31 +119,34 @@ async function parseErrorResponse(response) {
70
119
  const code = errPayload?.code || void 0;
71
120
  const fields = Array.isArray(errPayload?.fields) ? errPayload?.fields : void 0;
72
121
  const parsedStatus = typeof errPayload?.status === "number" ? errPayload.status : status;
73
- return new ModelRelayError(message, {
122
+ return new APIError(message, {
74
123
  status: parsedStatus,
75
124
  code,
76
125
  fields,
77
126
  requestId: parsedObj?.request_id || parsedObj?.requestId || requestId,
78
- data: parsed
127
+ data: parsed,
128
+ retries
79
129
  });
80
130
  }
81
131
  if (parsedObj?.message || parsedObj?.code) {
82
132
  const message = parsedObj.message || fallbackMessage;
83
- return new ModelRelayError(message, {
133
+ return new APIError(message, {
84
134
  status,
85
135
  code: parsedObj.code,
86
136
  fields: parsedObj.fields,
87
137
  requestId: parsedObj?.request_id || parsedObj?.requestId || requestId,
88
- data: parsed
138
+ data: parsed,
139
+ retries
89
140
  });
90
141
  }
91
- return new ModelRelayError(fallbackMessage, {
142
+ return new APIError(fallbackMessage, {
92
143
  status,
93
144
  requestId,
94
- data: parsed
145
+ data: parsed,
146
+ retries
95
147
  });
96
148
  } catch {
97
- return new ModelRelayError(bodyText, { status, requestId });
149
+ return new APIError(bodyText, { status, requestId, retries });
98
150
  }
99
151
  }
100
152
 
@@ -105,7 +157,7 @@ var AuthClient = class {
105
157
  this.http = http;
106
158
  this.apiKey = cfg.apiKey;
107
159
  this.accessToken = cfg.accessToken;
108
- this.endUser = cfg.endUser;
160
+ this.customer = cfg.customer;
109
161
  }
110
162
  /**
111
163
  * Exchange a publishable key for a short-lived frontend token.
@@ -114,28 +166,22 @@ var AuthClient = class {
114
166
  async frontendToken(request) {
115
167
  const publishableKey = request?.publishableKey || (isPublishableKey(this.apiKey) ? this.apiKey : void 0);
116
168
  if (!publishableKey) {
117
- throw new ModelRelayError(
118
- "publishable key required to issue frontend tokens",
119
- { status: 400 }
120
- );
169
+ throw new ConfigError("publishable key required to issue frontend tokens");
121
170
  }
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
- );
171
+ const customerId = request?.customerId || this.customer?.id;
172
+ if (!customerId) {
173
+ throw new ConfigError("customerId is required to mint a frontend token");
128
174
  }
129
- const deviceId = request?.deviceId || this.endUser?.deviceId;
130
- const ttlSeconds = request?.ttlSeconds ?? this.endUser?.ttlSeconds;
131
- const cacheKey = `${publishableKey}:${userId}:${deviceId || ""}`;
175
+ const deviceId = request?.deviceId || this.customer?.deviceId;
176
+ const ttlSeconds = request?.ttlSeconds ?? this.customer?.ttlSeconds;
177
+ const cacheKey = `${publishableKey}:${customerId}:${deviceId || ""}`;
132
178
  const cached = this.cachedFrontend.get(cacheKey);
133
179
  if (cached && isTokenReusable(cached)) {
134
180
  return cached;
135
181
  }
136
182
  const payload = {
137
183
  publishable_key: publishableKey,
138
- user_id: userId
184
+ customer_id: customerId
139
185
  };
140
186
  if (deviceId) {
141
187
  payload.device_id = deviceId;
@@ -152,7 +198,7 @@ var AuthClient = class {
152
198
  );
153
199
  const token = normalizeFrontendToken(response, {
154
200
  publishableKey,
155
- userId,
201
+ customerId,
156
202
  deviceId
157
203
  });
158
204
  this.cachedFrontend.set(cacheKey, token);
@@ -162,18 +208,16 @@ var AuthClient = class {
162
208
  * Determine the correct auth headers for chat completions.
163
209
  * Publishable keys are automatically exchanged for frontend tokens.
164
210
  */
165
- async authForChat(endUserId, overrides) {
211
+ async authForChat(customerId, overrides) {
166
212
  if (this.accessToken) {
167
213
  return { accessToken: this.accessToken };
168
214
  }
169
215
  if (!this.apiKey) {
170
- throw new ModelRelayError("API key or token is required", {
171
- status: 401
172
- });
216
+ throw new ConfigError("API key or token is required");
173
217
  }
174
218
  if (isPublishableKey(this.apiKey)) {
175
219
  const token = await this.frontendToken({
176
- userId: endUserId || overrides?.id,
220
+ customerId: customerId || overrides?.id,
177
221
  deviceId: overrides?.deviceId,
178
222
  ttlSeconds: overrides?.ttlSeconds
179
223
  });
@@ -189,9 +233,7 @@ var AuthClient = class {
189
233
  return { accessToken: this.accessToken };
190
234
  }
191
235
  if (!this.apiKey) {
192
- throw new ModelRelayError("API key or token is required", {
193
- status: 401
194
- });
236
+ throw new ConfigError("API key or token is required");
195
237
  }
196
238
  return { apiKey: this.apiKey };
197
239
  }
@@ -203,17 +245,17 @@ function isPublishableKey(value) {
203
245
  return value.trim().toLowerCase().startsWith("mr_pk_");
204
246
  }
205
247
  function normalizeFrontendToken(payload, meta) {
206
- const expiresAt = payload.expires_at || payload.expiresAt;
248
+ const expiresAt = payload.expires_at;
207
249
  return {
208
250
  token: payload.token,
209
251
  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,
252
+ expiresIn: payload.expires_in,
253
+ tokenType: payload.token_type,
254
+ keyId: payload.key_id,
255
+ sessionId: payload.session_id,
256
+ tokenScope: payload.token_scope,
257
+ tokenSource: payload.token_source,
258
+ customerId: meta.customerId,
217
259
  publishableKey: meta.publishableKey,
218
260
  deviceId: meta.deviceId
219
261
  };
@@ -228,120 +270,149 @@ function isTokenReusable(token) {
228
270
  return token.expiresAt.getTime() - Date.now() > 6e4;
229
271
  }
230
272
 
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 });
273
+ // package.json
274
+ var package_default = {
275
+ name: "@modelrelay/sdk",
276
+ version: "0.5.0",
277
+ description: "TypeScript SDK for the ModelRelay API",
278
+ type: "module",
279
+ main: "dist/index.cjs",
280
+ module: "dist/index.js",
281
+ types: "dist/index.d.ts",
282
+ exports: {
283
+ ".": {
284
+ types: "./dist/index.d.ts",
285
+ import: "./dist/index.js",
286
+ require: "./dist/index.cjs"
269
287
  }
270
- await this.http.request(`/api-keys/${encodeURIComponent(id)}`, {
271
- method: "DELETE"
272
- });
288
+ },
289
+ publishConfig: { access: "public" },
290
+ files: [
291
+ "dist"
292
+ ],
293
+ scripts: {
294
+ build: "tsup src/index.ts --format esm,cjs --dts",
295
+ dev: "tsup src/index.ts --format esm,cjs --dts --watch",
296
+ lint: "tsc --noEmit",
297
+ test: "vitest run"
298
+ },
299
+ keywords: [
300
+ "modelrelay",
301
+ "llm",
302
+ "sdk",
303
+ "typescript"
304
+ ],
305
+ author: "Shane Vitarana",
306
+ license: "Apache-2.0",
307
+ devDependencies: {
308
+ tsup: "^8.2.4",
309
+ typescript: "^5.6.3",
310
+ vitest: "^2.1.4"
273
311
  }
274
312
  };
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;
313
+
314
+ // src/types.ts
315
+ var SDK_VERSION = package_default.version || "0.0.0";
316
+ var DEFAULT_BASE_URL = "https://api.modelrelay.ai/api/v1";
317
+ var STAGING_BASE_URL = "https://api-stg.modelrelay.ai/api/v1";
318
+ var SANDBOX_BASE_URL = "https://api.sandbox.modelrelay.ai/api/v1";
319
+ var DEFAULT_CLIENT_HEADER = `modelrelay-ts/${SDK_VERSION}`;
320
+ var DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
321
+ var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
322
+ var StopReasons = {
323
+ Completed: "completed",
324
+ Stop: "stop",
325
+ StopSequence: "stop_sequence",
326
+ EndTurn: "end_turn",
327
+ MaxTokens: "max_tokens",
328
+ MaxLength: "max_len",
329
+ MaxContext: "max_context",
330
+ ToolCalls: "tool_calls",
331
+ TimeLimit: "time_limit",
332
+ ContentFilter: "content_filter",
333
+ Incomplete: "incomplete",
334
+ Unknown: "unknown"
335
+ };
336
+ var Providers = {
337
+ OpenAI: "openai",
338
+ Anthropic: "anthropic",
339
+ Grok: "grok",
340
+ OpenRouter: "openrouter",
341
+ Echo: "echo"
342
+ };
343
+ var Models = {
344
+ OpenAIGpt4o: "openai/gpt-4o",
345
+ OpenAIGpt4oMini: "openai/gpt-4o-mini",
346
+ OpenAIGpt51: "openai/gpt-5.1",
347
+ AnthropicClaude35HaikuLatest: "anthropic/claude-3-5-haiku-latest",
348
+ AnthropicClaude35SonnetLatest: "anthropic/claude-3-5-sonnet-latest",
349
+ AnthropicClaudeOpus45: "anthropic/claude-opus-4-5-20251101",
350
+ OpenRouterClaude35Haiku: "anthropic/claude-3.5-haiku",
351
+ Grok2: "grok-2",
352
+ Grok4Fast: "grok-4-fast",
353
+ Echo1: "echo-1"
354
+ };
355
+ function mergeMetrics(base, override) {
356
+ if (!base && !override) return void 0;
279
357
  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
358
+ ...base || {},
359
+ ...override || {}
288
360
  };
289
361
  }
290
-
291
- // src/billing.ts
292
- var BillingClient = class {
293
- constructor(http, auth) {
294
- this.http = http;
295
- this.auth = auth;
362
+ function mergeTrace(base, override) {
363
+ if (!base && !override) return void 0;
364
+ return {
365
+ ...base || {},
366
+ ...override || {}
367
+ };
368
+ }
369
+ function normalizeStopReason(value) {
370
+ if (value === void 0 || value === null) return void 0;
371
+ const str = String(value).trim();
372
+ const lower = str.toLowerCase();
373
+ for (const reason of Object.values(StopReasons)) {
374
+ if (lower === reason) return reason;
296
375
  }
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);
376
+ switch (lower) {
377
+ case "length":
378
+ return StopReasons.MaxLength;
379
+ default:
380
+ return { other: str };
328
381
  }
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 };
382
+ }
383
+ function stopReasonToString(value) {
384
+ if (!value) return void 0;
385
+ if (typeof value === "string") return value;
386
+ return value.other?.trim() || void 0;
387
+ }
388
+ function normalizeProvider(value) {
389
+ if (value === void 0 || value === null) return void 0;
390
+ const str = String(value).trim();
391
+ if (!str) return void 0;
392
+ const lower = str.toLowerCase();
393
+ for (const p of Object.values(Providers)) {
394
+ if (lower === p) return p;
395
+ }
396
+ return { other: str };
397
+ }
398
+ function providerToString(value) {
399
+ if (!value) return void 0;
400
+ if (typeof value === "string") return value;
401
+ return value.other?.trim() || void 0;
402
+ }
403
+ function normalizeModelId(value) {
404
+ if (value === void 0 || value === null) return void 0;
405
+ const str = String(value).trim();
406
+ if (!str) return void 0;
407
+ const lower = str.toLowerCase();
408
+ for (const m of Object.values(Models)) {
409
+ if (lower === m) return m;
410
+ }
411
+ return { other: str };
412
+ }
413
+ function modelToString(value) {
414
+ if (typeof value === "string") return value;
415
+ return value.other?.trim() || "";
345
416
  }
346
417
 
347
418
  // src/chat.ts
@@ -351,33 +422,35 @@ var ChatClient = class {
351
422
  this.completions = new ChatCompletionsClient(
352
423
  http,
353
424
  auth,
354
- cfg.defaultMetadata
425
+ cfg.defaultMetadata,
426
+ cfg.metrics,
427
+ cfg.trace
355
428
  );
356
429
  }
357
430
  };
358
431
  var ChatCompletionsClient = class {
359
- constructor(http, auth, defaultMetadata) {
432
+ constructor(http, auth, defaultMetadata, metrics, trace) {
360
433
  this.http = http;
361
434
  this.auth = auth;
362
435
  this.defaultMetadata = defaultMetadata;
436
+ this.metrics = metrics;
437
+ this.trace = trace;
363
438
  }
364
439
  async create(params, options = {}) {
365
440
  const stream = options.stream ?? params.stream ?? true;
366
- if (!params?.model?.trim()) {
367
- throw new ModelRelayError("model is required", { status: 400 });
441
+ const metrics = mergeMetrics(this.metrics, options.metrics);
442
+ const trace = mergeTrace(this.trace, options.trace);
443
+ const modelValue = modelToString(params.model).trim();
444
+ if (!modelValue) {
445
+ throw new ConfigError("model is required");
368
446
  }
369
447
  if (!params?.messages?.length) {
370
- throw new ModelRelayError("at least one message is required", {
371
- status: 400
372
- });
448
+ throw new ConfigError("at least one message is required");
373
449
  }
374
450
  if (!hasUserMessage(params.messages)) {
375
- throw new ModelRelayError(
376
- "at least one user message is required",
377
- { status: 400 }
378
- );
451
+ throw new ConfigError("at least one user message is required");
379
452
  }
380
- const authHeaders = await this.auth.authForChat(params.endUserId);
453
+ const authHeaders = await this.auth.authForChat(params.customerId);
381
454
  const body = buildProxyBody(
382
455
  params,
383
456
  mergeMetadata(this.defaultMetadata, params.metadata, options.metadata)
@@ -387,6 +460,13 @@ var ChatCompletionsClient = class {
387
460
  if (requestId) {
388
461
  headers[REQUEST_ID_HEADER] = requestId;
389
462
  }
463
+ const baseContext = {
464
+ method: "POST",
465
+ path: "/llm/proxy",
466
+ provider: params.provider,
467
+ model: params.model,
468
+ requestId
469
+ };
390
470
  const response = await this.http.request("/llm/proxy", {
391
471
  method: "POST",
392
472
  body,
@@ -398,7 +478,11 @@ var ChatCompletionsClient = class {
398
478
  signal: options.signal,
399
479
  timeoutMs: options.timeoutMs ?? (stream ? 0 : void 0),
400
480
  useDefaultTimeout: !stream,
401
- retry: options.retry
481
+ connectTimeoutMs: options.connectTimeoutMs,
482
+ retry: options.retry,
483
+ metrics,
484
+ trace,
485
+ context: baseContext
402
486
  });
403
487
  const resolvedRequestId = requestIdFromHeaders(response.headers) || requestId || void 0;
404
488
  if (!response.ok) {
@@ -406,21 +490,43 @@ var ChatCompletionsClient = class {
406
490
  }
407
491
  if (!stream) {
408
492
  const payload = await response.json();
409
- return normalizeChatResponse(payload, resolvedRequestId);
493
+ const result = normalizeChatResponse(payload, resolvedRequestId);
494
+ if (metrics?.usage) {
495
+ const ctx = {
496
+ ...baseContext,
497
+ requestId: resolvedRequestId ?? baseContext.requestId,
498
+ responseId: result.id
499
+ };
500
+ metrics.usage({ usage: result.usage, context: ctx });
501
+ }
502
+ return result;
410
503
  }
411
- return new ChatCompletionsStream(response, resolvedRequestId);
504
+ const streamContext = {
505
+ ...baseContext,
506
+ requestId: resolvedRequestId ?? baseContext.requestId
507
+ };
508
+ return new ChatCompletionsStream(
509
+ response,
510
+ resolvedRequestId,
511
+ streamContext,
512
+ metrics,
513
+ trace
514
+ );
412
515
  }
413
516
  };
414
517
  var ChatCompletionsStream = class {
415
- constructor(response, requestId) {
518
+ constructor(response, requestId, context, metrics, trace) {
519
+ this.firstTokenEmitted = false;
416
520
  this.closed = false;
417
521
  if (!response.body) {
418
- throw new ModelRelayError("streaming response is missing a body", {
419
- status: 500
420
- });
522
+ throw new ConfigError("streaming response is missing a body");
421
523
  }
422
524
  this.response = response;
423
525
  this.requestId = requestId;
526
+ this.context = context;
527
+ this.metrics = metrics;
528
+ this.trace = trace;
529
+ this.startedAt = this.metrics?.streamFirstToken || this.trace?.streamEvent || this.trace?.streamError ? Date.now() : 0;
424
530
  }
425
531
  async cancel(reason) {
426
532
  this.closed = true;
@@ -435,9 +541,7 @@ var ChatCompletionsStream = class {
435
541
  }
436
542
  const body = this.response.body;
437
543
  if (!body) {
438
- throw new ModelRelayError("streaming response is missing a body", {
439
- status: 500
440
- });
544
+ throw new ConfigError("streaming response is missing a body");
441
545
  }
442
546
  const reader = body.getReader();
443
547
  const decoder = new TextDecoder();
@@ -454,6 +558,7 @@ var ChatCompletionsStream = class {
454
558
  for (const evt of events2) {
455
559
  const parsed = mapChatEvent(evt, this.requestId);
456
560
  if (parsed) {
561
+ this.handleStreamEvent(parsed);
457
562
  yield parsed;
458
563
  }
459
564
  }
@@ -465,15 +570,49 @@ var ChatCompletionsStream = class {
465
570
  for (const evt of events) {
466
571
  const parsed = mapChatEvent(evt, this.requestId);
467
572
  if (parsed) {
573
+ this.handleStreamEvent(parsed);
468
574
  yield parsed;
469
575
  }
470
576
  }
471
577
  }
578
+ } catch (err) {
579
+ this.recordFirstToken(err);
580
+ this.trace?.streamError?.({ context: this.context, error: err });
581
+ throw err;
472
582
  } finally {
473
583
  this.closed = true;
474
584
  reader.releaseLock();
475
585
  }
476
586
  }
587
+ handleStreamEvent(evt) {
588
+ const context = this.enrichContext(evt);
589
+ this.context = context;
590
+ this.trace?.streamEvent?.({ context, event: evt });
591
+ if (evt.type === "message_start" || evt.type === "message_delta" || evt.type === "message_stop") {
592
+ this.recordFirstToken();
593
+ }
594
+ if (evt.type === "message_stop" && evt.usage && this.metrics?.usage) {
595
+ this.metrics.usage({ usage: evt.usage, context });
596
+ }
597
+ }
598
+ enrichContext(evt) {
599
+ return {
600
+ ...this.context,
601
+ responseId: evt.responseId || this.context.responseId,
602
+ requestId: evt.requestId || this.context.requestId,
603
+ model: evt.model || this.context.model
604
+ };
605
+ }
606
+ recordFirstToken(error) {
607
+ if (!this.metrics?.streamFirstToken || this.firstTokenEmitted) return;
608
+ this.firstTokenEmitted = true;
609
+ const latencyMs = this.startedAt ? Date.now() - this.startedAt : 0;
610
+ this.metrics.streamFirstToken({
611
+ latencyMs,
612
+ error: error ? String(error) : void 0,
613
+ context: this.context
614
+ });
615
+ }
477
616
  };
478
617
  function consumeSSEBuffer(buffer, flush = false) {
479
618
  const events = [];
@@ -530,9 +669,9 @@ function mapChatEvent(raw, requestId) {
530
669
  const p = payload;
531
670
  const type = normalizeEventType(raw.event, p);
532
671
  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;
672
+ const responseId = p.response_id || p.id || p?.message?.id;
673
+ const model = normalizeModelId(p.model || p?.message?.model);
674
+ const stopReason = normalizeStopReason(p.stop_reason);
536
675
  const textDelta = extractTextDelta(p);
537
676
  return {
538
677
  type,
@@ -585,10 +724,10 @@ function normalizeChatResponse(payload, requestId) {
585
724
  const p = payload;
586
725
  return {
587
726
  id: p?.id,
588
- provider: p?.provider,
727
+ provider: normalizeProvider(p?.provider),
589
728
  content: Array.isArray(p?.content) ? p.content : p?.content ? [String(p.content)] : [],
590
- stopReason: p?.stop_reason ?? p?.stopReason,
591
- model: p?.model,
729
+ stopReason: normalizeStopReason(p?.stop_reason),
730
+ model: normalizeModelId(p?.model),
592
731
  usage: normalizeUsage(p?.usage),
593
732
  requestId
594
733
  };
@@ -597,19 +736,23 @@ function normalizeUsage(payload) {
597
736
  if (!payload) {
598
737
  return { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
599
738
  }
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)
739
+ const usage = {
740
+ inputTokens: Number(payload.input_tokens ?? 0),
741
+ outputTokens: Number(payload.output_tokens ?? 0),
742
+ totalTokens: Number(payload.total_tokens ?? 0)
604
743
  };
744
+ if (!usage.totalTokens) {
745
+ usage.totalTokens = usage.inputTokens + usage.outputTokens;
746
+ }
747
+ return usage;
605
748
  }
606
749
  function buildProxyBody(params, metadata) {
607
750
  const body = {
608
- model: params.model,
751
+ model: modelToString(params.model),
609
752
  messages: normalizeMessages(params.messages)
610
753
  };
611
754
  if (typeof params.maxTokens === "number") body.max_tokens = params.maxTokens;
612
- if (params.provider) body.provider = params.provider;
755
+ if (params.provider) body.provider = providerToString(params.provider);
613
756
  if (typeof params.temperature === "number")
614
757
  body.temperature = params.temperature;
615
758
  if (metadata && Object.keys(metadata).length > 0) body.metadata = metadata;
@@ -645,78 +788,218 @@ function hasUserMessage(messages) {
645
788
  );
646
789
  }
647
790
 
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"
791
+ // src/customers.ts
792
+ var CustomersClient = class {
793
+ constructor(http, cfg) {
794
+ this.http = http;
795
+ this.apiKey = cfg.apiKey;
796
+ }
797
+ ensureSecretKey() {
798
+ if (!this.apiKey || !this.apiKey.startsWith("mr_sk_")) {
799
+ throw new ConfigError(
800
+ "Secret key (mr_sk_*) required for customer operations"
801
+ );
662
802
  }
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"
803
+ }
804
+ /**
805
+ * List all customers in the project.
806
+ */
807
+ async list() {
808
+ this.ensureSecretKey();
809
+ const response = await this.http.json("/customers", {
810
+ method: "GET",
811
+ apiKey: this.apiKey
812
+ });
813
+ return response.customers;
814
+ }
815
+ /**
816
+ * Create a new customer in the project.
817
+ */
818
+ async create(request) {
819
+ this.ensureSecretKey();
820
+ if (!request.tier_id?.trim()) {
821
+ throw new ConfigError("tier_id is required");
822
+ }
823
+ if (!request.external_id?.trim()) {
824
+ throw new ConfigError("external_id is required");
825
+ }
826
+ const response = await this.http.json("/customers", {
827
+ method: "POST",
828
+ body: request,
829
+ apiKey: this.apiKey
830
+ });
831
+ return response.customer;
832
+ }
833
+ /**
834
+ * Get a customer by ID.
835
+ */
836
+ async get(customerId) {
837
+ this.ensureSecretKey();
838
+ if (!customerId?.trim()) {
839
+ throw new ConfigError("customerId is required");
840
+ }
841
+ const response = await this.http.json(
842
+ `/customers/${customerId}`,
843
+ {
844
+ method: "GET",
845
+ apiKey: this.apiKey
846
+ }
847
+ );
848
+ return response.customer;
849
+ }
850
+ /**
851
+ * Upsert a customer by external_id.
852
+ * If a customer with the given external_id exists, it is updated.
853
+ * Otherwise, a new customer is created.
854
+ */
855
+ async upsert(request) {
856
+ this.ensureSecretKey();
857
+ if (!request.tier_id?.trim()) {
858
+ throw new ConfigError("tier_id is required");
859
+ }
860
+ if (!request.external_id?.trim()) {
861
+ throw new ConfigError("external_id is required");
862
+ }
863
+ const response = await this.http.json("/customers", {
864
+ method: "PUT",
865
+ body: request,
866
+ apiKey: this.apiKey
867
+ });
868
+ return response.customer;
869
+ }
870
+ /**
871
+ * Delete a customer by ID.
872
+ */
873
+ async delete(customerId) {
874
+ this.ensureSecretKey();
875
+ if (!customerId?.trim()) {
876
+ throw new ConfigError("customerId is required");
877
+ }
878
+ await this.http.request(`/customers/${customerId}`, {
879
+ method: "DELETE",
880
+ apiKey: this.apiKey
881
+ });
882
+ }
883
+ /**
884
+ * Create a Stripe checkout session for a customer.
885
+ */
886
+ async createCheckoutSession(customerId, request) {
887
+ this.ensureSecretKey();
888
+ if (!customerId?.trim()) {
889
+ throw new ConfigError("customerId is required");
890
+ }
891
+ if (!request.success_url?.trim() || !request.cancel_url?.trim()) {
892
+ throw new ConfigError("success_url and cancel_url are required");
893
+ }
894
+ return await this.http.json(
895
+ `/customers/${customerId}/checkout`,
896
+ {
897
+ method: "POST",
898
+ body: request,
899
+ apiKey: this.apiKey
900
+ }
901
+ );
902
+ }
903
+ /**
904
+ * Get the subscription status for a customer.
905
+ */
906
+ async getSubscription(customerId) {
907
+ this.ensureSecretKey();
908
+ if (!customerId?.trim()) {
909
+ throw new ConfigError("customerId is required");
910
+ }
911
+ return await this.http.json(
912
+ `/customers/${customerId}/subscription`,
913
+ {
914
+ method: "GET",
915
+ apiKey: this.apiKey
916
+ }
917
+ );
686
918
  }
687
919
  };
688
920
 
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;
921
+ // src/tiers.ts
922
+ var TiersClient = class {
923
+ constructor(http, cfg) {
924
+ this.http = http;
925
+ this.apiKey = cfg.apiKey;
926
+ }
927
+ ensureApiKey() {
928
+ if (!this.apiKey || !this.apiKey.startsWith("mr_pk_") && !this.apiKey.startsWith("mr_sk_")) {
929
+ throw new ConfigError(
930
+ "API key (mr_pk_* or mr_sk_*) required for tier operations"
931
+ );
932
+ }
933
+ }
934
+ /**
935
+ * List all tiers in the project.
936
+ */
937
+ async list() {
938
+ this.ensureApiKey();
939
+ const response = await this.http.json("/tiers", {
940
+ method: "GET",
941
+ apiKey: this.apiKey
942
+ });
943
+ return response.tiers;
944
+ }
945
+ /**
946
+ * Get a tier by ID.
947
+ */
948
+ async get(tierId) {
949
+ this.ensureApiKey();
950
+ if (!tierId?.trim()) {
951
+ throw new ConfigError("tierId is required");
952
+ }
953
+ const response = await this.http.json(`/tiers/${tierId}`, {
954
+ method: "GET",
955
+ apiKey: this.apiKey
956
+ });
957
+ return response.tier;
958
+ }
959
+ };
696
960
 
697
961
  // src/http.ts
698
962
  var HTTPClient = class {
699
963
  constructor(cfg) {
700
964
  const baseFromEnv = baseUrlForEnvironment(cfg.environment);
701
- this.baseUrl = normalizeBaseUrl(cfg.baseUrl || baseFromEnv || DEFAULT_BASE_URL);
965
+ const resolvedBase = normalizeBaseUrl(
966
+ cfg.baseUrl || baseFromEnv || DEFAULT_BASE_URL
967
+ );
968
+ if (!isValidHttpUrl(resolvedBase)) {
969
+ throw new ConfigError(
970
+ "baseUrl must start with http:// or https://"
971
+ );
972
+ }
973
+ this.baseUrl = resolvedBase;
702
974
  this.apiKey = cfg.apiKey?.trim();
703
975
  this.accessToken = cfg.accessToken?.trim();
704
976
  this.fetchImpl = cfg.fetchImpl;
705
977
  this.clientHeader = cfg.clientHeader?.trim() || DEFAULT_CLIENT_HEADER;
978
+ this.defaultConnectTimeoutMs = cfg.connectTimeoutMs === void 0 ? DEFAULT_CONNECT_TIMEOUT_MS : Math.max(0, cfg.connectTimeoutMs);
706
979
  this.defaultTimeoutMs = cfg.timeoutMs === void 0 ? DEFAULT_REQUEST_TIMEOUT_MS : Math.max(0, cfg.timeoutMs);
707
980
  this.retry = normalizeRetryConfig(cfg.retry);
708
981
  this.defaultHeaders = normalizeHeaders(cfg.defaultHeaders);
982
+ this.metrics = cfg.metrics;
983
+ this.trace = cfg.trace;
709
984
  }
710
985
  async request(path, options = {}) {
711
986
  const fetchFn = this.fetchImpl ?? globalThis.fetch;
712
987
  if (!fetchFn) {
713
- throw new ModelRelayError(
714
- "fetch is not available; provide a fetch implementation",
715
- { status: 500 }
988
+ throw new ConfigError(
989
+ "fetch is not available; provide a fetch implementation"
716
990
  );
717
991
  }
718
992
  const method = options.method || "GET";
719
993
  const url = buildUrl(this.baseUrl, path);
994
+ const metrics = mergeMetrics(this.metrics, options.metrics);
995
+ const trace = mergeTrace(this.trace, options.trace);
996
+ const context = {
997
+ method,
998
+ path,
999
+ ...options.context || {}
1000
+ };
1001
+ trace?.requestStart?.(context);
1002
+ const start = metrics?.httpRequest || trace?.requestFinish ? Date.now() : 0;
720
1003
  const headers = new Headers({
721
1004
  ...this.defaultHeaders,
722
1005
  ...options.headers || {}
@@ -744,15 +1027,35 @@ var HTTPClient = class {
744
1027
  headers.set("X-ModelRelay-Client", this.clientHeader);
745
1028
  }
746
1029
  const timeoutMs = options.useDefaultTimeout === false ? options.timeoutMs : options.timeoutMs ?? this.defaultTimeoutMs;
1030
+ const connectTimeoutMs = options.useDefaultConnectTimeout === false ? options.connectTimeoutMs : options.connectTimeoutMs ?? this.defaultConnectTimeoutMs;
747
1031
  const retryCfg = normalizeRetryConfig(
748
1032
  options.retry === void 0 ? this.retry : options.retry
749
1033
  );
750
1034
  const attempts = retryCfg ? Math.max(1, retryCfg.maxAttempts) : 1;
751
1035
  let lastError;
1036
+ let lastStatus;
752
1037
  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);
1038
+ let connectTimedOut = false;
1039
+ let requestTimedOut = false;
1040
+ const connectController = connectTimeoutMs && connectTimeoutMs > 0 ? new AbortController() : void 0;
1041
+ const requestController = timeoutMs && timeoutMs > 0 ? new AbortController() : void 0;
1042
+ const signal = mergeSignals(
1043
+ options.signal,
1044
+ connectController?.signal,
1045
+ requestController?.signal
1046
+ );
1047
+ const connectTimer = connectController && setTimeout(() => {
1048
+ connectTimedOut = true;
1049
+ connectController.abort(
1050
+ new DOMException("connect timeout", "AbortError")
1051
+ );
1052
+ }, connectTimeoutMs);
1053
+ const requestTimer = requestController && setTimeout(() => {
1054
+ requestTimedOut = true;
1055
+ requestController.abort(
1056
+ new DOMException("timeout", "AbortError")
1057
+ );
1058
+ }, timeoutMs);
756
1059
  try {
757
1060
  const response = await fetchFn(url, {
758
1061
  method,
@@ -760,6 +1063,9 @@ var HTTPClient = class {
760
1063
  body: payload,
761
1064
  signal
762
1065
  });
1066
+ if (connectTimer) {
1067
+ clearTimeout(connectTimer);
1068
+ }
763
1069
  if (!response.ok) {
764
1070
  const shouldRetry = retryCfg && shouldRetryStatus(
765
1071
  response.status,
@@ -767,31 +1073,68 @@ var HTTPClient = class {
767
1073
  retryCfg.retryPost
768
1074
  ) && attempt < attempts;
769
1075
  if (shouldRetry) {
1076
+ lastStatus = response.status;
770
1077
  await backoff(attempt, retryCfg);
771
1078
  continue;
772
1079
  }
773
- if (!options.raw) {
774
- throw await parseErrorResponse(response);
775
- }
1080
+ const retries = buildRetryMetadata(attempt, response.status, lastError);
1081
+ const finishedCtx2 = withRequestId(context, response.headers);
1082
+ recordHttpMetrics(metrics, trace, start, retries, {
1083
+ status: response.status,
1084
+ context: finishedCtx2
1085
+ });
1086
+ throw options.raw ? await parseErrorResponse(response, retries) : await parseErrorResponse(response, retries);
776
1087
  }
1088
+ const finishedCtx = withRequestId(context, response.headers);
1089
+ recordHttpMetrics(metrics, trace, start, void 0, {
1090
+ status: response.status,
1091
+ context: finishedCtx
1092
+ });
777
1093
  return response;
778
1094
  } catch (err) {
779
1095
  if (options.signal?.aborted) {
780
1096
  throw err;
781
1097
  }
782
- const shouldRetry = retryCfg && isRetryableError(err) && (method !== "POST" || retryCfg.retryPost) && attempt < attempts;
783
- if (!shouldRetry) {
1098
+ if (err instanceof ModelRelayError) {
1099
+ recordHttpMetrics(metrics, trace, start, void 0, {
1100
+ error: err,
1101
+ context
1102
+ });
784
1103
  throw err;
785
1104
  }
1105
+ const transportKind = classifyTransportErrorKind(
1106
+ err,
1107
+ connectTimedOut,
1108
+ requestTimedOut
1109
+ );
1110
+ const shouldRetry = retryCfg && isRetryableError(err, transportKind) && (method !== "POST" || retryCfg.retryPost) && attempt < attempts;
1111
+ if (!shouldRetry) {
1112
+ const retries = buildRetryMetadata(
1113
+ attempt,
1114
+ lastStatus,
1115
+ err instanceof Error ? err.message : String(err)
1116
+ );
1117
+ recordHttpMetrics(metrics, trace, start, retries, {
1118
+ error: err,
1119
+ context
1120
+ });
1121
+ throw toTransportError(err, transportKind, retries);
1122
+ }
786
1123
  lastError = err;
787
1124
  await backoff(attempt, retryCfg);
788
1125
  } finally {
789
- if (timer) {
790
- clearTimeout(timer);
1126
+ if (connectTimer) {
1127
+ clearTimeout(connectTimer);
1128
+ }
1129
+ if (requestTimer) {
1130
+ clearTimeout(requestTimer);
791
1131
  }
792
1132
  }
793
1133
  }
794
- throw lastError instanceof Error ? lastError : new ModelRelayError("request failed", { status: 500 });
1134
+ throw lastError instanceof Error ? lastError : new TransportError("request failed", {
1135
+ kind: "other",
1136
+ retries: buildRetryMetadata(attempts, lastStatus)
1137
+ });
795
1138
  }
796
1139
  async json(path, options = {}) {
797
1140
  const response = await this.request(path, {
@@ -808,7 +1151,7 @@ var HTTPClient = class {
808
1151
  try {
809
1152
  return await response.json();
810
1153
  } catch (err) {
811
- throw new ModelRelayError("failed to parse response JSON", {
1154
+ throw new APIError("failed to parse response JSON", {
812
1155
  status: response.status,
813
1156
  data: err
814
1157
  });
@@ -831,6 +1174,9 @@ function normalizeBaseUrl(value) {
831
1174
  }
832
1175
  return trimmed;
833
1176
  }
1177
+ function isValidHttpUrl(value) {
1178
+ return /^https?:\/\//i.test(value);
1179
+ }
834
1180
  function baseUrlForEnvironment(env) {
835
1181
  if (!env || env === "production") return void 0;
836
1182
  if (env === "staging") return STAGING_BASE_URL;
@@ -856,9 +1202,10 @@ function shouldRetryStatus(status, method, retryPost) {
856
1202
  }
857
1203
  return false;
858
1204
  }
859
- function isRetryableError(err) {
1205
+ function isRetryableError(err, kind) {
860
1206
  if (!err) return false;
861
- return err instanceof DOMException && err.name === "AbortError" || err instanceof TypeError;
1207
+ if (kind === "timeout" || kind === "connect") return true;
1208
+ return err instanceof DOMException || err instanceof TypeError;
862
1209
  }
863
1210
  function backoff(attempt, cfg) {
864
1211
  const exp = Math.max(0, attempt - 1);
@@ -869,24 +1216,22 @@ function backoff(attempt, cfg) {
869
1216
  if (delay <= 0) return Promise.resolve();
870
1217
  return new Promise((resolve) => setTimeout(resolve, delay));
871
1218
  }
872
- function mergeSignals(user, timeoutSignal) {
873
- if (!user && !timeoutSignal) return void 0;
874
- if (user && !timeoutSignal) return user;
875
- if (!user && timeoutSignal) return timeoutSignal;
1219
+ function mergeSignals(...signals) {
1220
+ const active = signals.filter(Boolean);
1221
+ if (active.length === 0) return void 0;
1222
+ if (active.length === 1) return active[0];
876
1223
  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
- );
1224
+ for (const src of active) {
1225
+ if (src.aborted) {
1226
+ controller.abort(src.reason);
1227
+ break;
886
1228
  }
887
- };
888
- if (user) propagate(user);
889
- if (timeoutSignal) propagate(timeoutSignal);
1229
+ src.addEventListener(
1230
+ "abort",
1231
+ () => controller.abort(src.reason),
1232
+ { once: true }
1233
+ );
1234
+ }
890
1235
  return controller.signal;
891
1236
  }
892
1237
  function normalizeHeaders(headers) {
@@ -902,15 +1247,59 @@ function normalizeHeaders(headers) {
902
1247
  }
903
1248
  return normalized;
904
1249
  }
1250
+ function buildRetryMetadata(attempt, lastStatus, lastError) {
1251
+ if (!attempt || attempt <= 1) return void 0;
1252
+ return {
1253
+ attempts: attempt,
1254
+ lastStatus,
1255
+ lastError: typeof lastError === "string" ? lastError : lastError instanceof Error ? lastError.message : lastError ? String(lastError) : void 0
1256
+ };
1257
+ }
1258
+ function classifyTransportErrorKind(err, connectTimedOut, requestTimedOut) {
1259
+ if (connectTimedOut) return "connect";
1260
+ if (requestTimedOut) return "timeout";
1261
+ if (err instanceof DOMException && err.name === "AbortError") {
1262
+ return requestTimedOut ? "timeout" : "request";
1263
+ }
1264
+ if (err instanceof TypeError) return "request";
1265
+ return "other";
1266
+ }
1267
+ function toTransportError(err, kind, retries) {
1268
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : "request failed";
1269
+ return new TransportError(message, { kind, retries, cause: err });
1270
+ }
1271
+ function recordHttpMetrics(metrics, trace, start, retries, info) {
1272
+ if (!metrics?.httpRequest && !trace?.requestFinish) return;
1273
+ const latencyMs = start ? Date.now() - start : 0;
1274
+ if (metrics?.httpRequest) {
1275
+ metrics.httpRequest({
1276
+ latencyMs,
1277
+ status: info.status,
1278
+ error: info.error ? String(info.error) : void 0,
1279
+ retries,
1280
+ context: info.context
1281
+ });
1282
+ }
1283
+ trace?.requestFinish?.({
1284
+ context: info.context,
1285
+ status: info.status,
1286
+ error: info.error,
1287
+ retries,
1288
+ latencyMs
1289
+ });
1290
+ }
1291
+ function withRequestId(context, headers) {
1292
+ const requestId = headers.get("X-ModelRelay-Chat-Request-Id") || headers.get("X-Request-Id") || context.requestId;
1293
+ if (!requestId) return context;
1294
+ return { ...context, requestId };
1295
+ }
905
1296
 
906
1297
  // src/index.ts
907
1298
  var ModelRelay = class {
908
1299
  constructor(options) {
909
1300
  const cfg = options || {};
910
1301
  if (!cfg.key && !cfg.token) {
911
- throw new ModelRelayError("Provide an API key or access token", {
912
- status: 400
913
- });
1302
+ throw new ConfigError("Provide an API key or access token");
914
1303
  }
915
1304
  this.baseUrl = resolveBaseUrl(cfg.environment, cfg.baseUrl);
916
1305
  const http = new HTTPClient({
@@ -919,22 +1308,31 @@ var ModelRelay = class {
919
1308
  accessToken: cfg.token,
920
1309
  fetchImpl: cfg.fetch,
921
1310
  clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
1311
+ connectTimeoutMs: cfg.connectTimeoutMs,
922
1312
  timeoutMs: cfg.timeoutMs,
923
1313
  retry: cfg.retry,
924
1314
  defaultHeaders: cfg.defaultHeaders,
925
- environment: cfg.environment
1315
+ environment: cfg.environment,
1316
+ metrics: cfg.metrics,
1317
+ trace: cfg.trace
926
1318
  });
927
1319
  const auth = new AuthClient(http, {
928
1320
  apiKey: cfg.key,
929
1321
  accessToken: cfg.token,
930
- endUser: cfg.endUser
1322
+ customer: cfg.customer
931
1323
  });
932
1324
  this.auth = auth;
933
- this.billing = new BillingClient(http, auth);
934
1325
  this.chat = new ChatClient(http, auth, {
935
- defaultMetadata: cfg.defaultMetadata
1326
+ defaultMetadata: cfg.defaultMetadata,
1327
+ metrics: cfg.metrics,
1328
+ trace: cfg.trace
1329
+ });
1330
+ this.customers = new CustomersClient(http, {
1331
+ apiKey: cfg.key
1332
+ });
1333
+ this.tiers = new TiersClient(http, {
1334
+ apiKey: cfg.key
936
1335
  });
937
- this.apiKeys = new ApiKeysClient(http);
938
1336
  }
939
1337
  };
940
1338
  function resolveBaseUrl(env, override) {
@@ -943,18 +1341,34 @@ function resolveBaseUrl(env, override) {
943
1341
  }
944
1342
  // Annotate the CommonJS export names for ESM import in node:
945
1343
  0 && (module.exports = {
946
- ApiKeysClient,
1344
+ APIError,
947
1345
  AuthClient,
948
- BillingClient,
949
1346
  ChatClient,
950
1347
  ChatCompletionsStream,
1348
+ ConfigError,
1349
+ CustomersClient,
951
1350
  DEFAULT_BASE_URL,
952
1351
  DEFAULT_CLIENT_HEADER,
1352
+ DEFAULT_CONNECT_TIMEOUT_MS,
953
1353
  DEFAULT_REQUEST_TIMEOUT_MS,
954
1354
  ModelRelay,
955
1355
  ModelRelayError,
1356
+ Models,
1357
+ Providers,
956
1358
  SANDBOX_BASE_URL,
957
1359
  SDK_VERSION,
958
1360
  STAGING_BASE_URL,
959
- isPublishableKey
1361
+ StopReasons,
1362
+ TiersClient,
1363
+ TransportError,
1364
+ isPublishableKey,
1365
+ mergeMetrics,
1366
+ mergeTrace,
1367
+ modelToString,
1368
+ normalizeModelId,
1369
+ normalizeProvider,
1370
+ normalizeStopReason,
1371
+ parseErrorResponse,
1372
+ providerToString,
1373
+ stopReasonToString
960
1374
  });