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