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