@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.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.4.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,178 @@ 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;
657
-
658
866
  // src/http.ts
659
867
  var HTTPClient = class {
660
868
  constructor(cfg) {
661
869
  const baseFromEnv = baseUrlForEnvironment(cfg.environment);
662
- this.baseUrl = normalizeBaseUrl(cfg.baseUrl || baseFromEnv || DEFAULT_BASE_URL);
870
+ const resolvedBase = normalizeBaseUrl(
871
+ cfg.baseUrl || baseFromEnv || DEFAULT_BASE_URL
872
+ );
873
+ if (!isValidHttpUrl(resolvedBase)) {
874
+ throw new ConfigError(
875
+ "baseUrl must start with http:// or https://"
876
+ );
877
+ }
878
+ this.baseUrl = resolvedBase;
663
879
  this.apiKey = cfg.apiKey?.trim();
664
880
  this.accessToken = cfg.accessToken?.trim();
665
881
  this.fetchImpl = cfg.fetchImpl;
666
882
  this.clientHeader = cfg.clientHeader?.trim() || DEFAULT_CLIENT_HEADER;
883
+ this.defaultConnectTimeoutMs = cfg.connectTimeoutMs === void 0 ? DEFAULT_CONNECT_TIMEOUT_MS : Math.max(0, cfg.connectTimeoutMs);
667
884
  this.defaultTimeoutMs = cfg.timeoutMs === void 0 ? DEFAULT_REQUEST_TIMEOUT_MS : Math.max(0, cfg.timeoutMs);
668
885
  this.retry = normalizeRetryConfig(cfg.retry);
669
886
  this.defaultHeaders = normalizeHeaders(cfg.defaultHeaders);
887
+ this.metrics = cfg.metrics;
888
+ this.trace = cfg.trace;
670
889
  }
671
890
  async request(path, options = {}) {
672
891
  const fetchFn = this.fetchImpl ?? globalThis.fetch;
673
892
  if (!fetchFn) {
674
- throw new ModelRelayError(
675
- "fetch is not available; provide a fetch implementation",
676
- { status: 500 }
893
+ throw new ConfigError(
894
+ "fetch is not available; provide a fetch implementation"
677
895
  );
678
896
  }
679
897
  const method = options.method || "GET";
680
898
  const url = buildUrl(this.baseUrl, path);
899
+ const metrics = mergeMetrics(this.metrics, options.metrics);
900
+ const trace = mergeTrace(this.trace, options.trace);
901
+ const context = {
902
+ method,
903
+ path,
904
+ ...options.context || {}
905
+ };
906
+ trace?.requestStart?.(context);
907
+ const start = metrics?.httpRequest || trace?.requestFinish ? Date.now() : 0;
681
908
  const headers = new Headers({
682
909
  ...this.defaultHeaders,
683
910
  ...options.headers || {}
@@ -705,15 +932,35 @@ var HTTPClient = class {
705
932
  headers.set("X-ModelRelay-Client", this.clientHeader);
706
933
  }
707
934
  const timeoutMs = options.useDefaultTimeout === false ? options.timeoutMs : options.timeoutMs ?? this.defaultTimeoutMs;
935
+ const connectTimeoutMs = options.useDefaultConnectTimeout === false ? options.connectTimeoutMs : options.connectTimeoutMs ?? this.defaultConnectTimeoutMs;
708
936
  const retryCfg = normalizeRetryConfig(
709
937
  options.retry === void 0 ? this.retry : options.retry
710
938
  );
711
939
  const attempts = retryCfg ? Math.max(1, retryCfg.maxAttempts) : 1;
712
940
  let lastError;
941
+ let lastStatus;
713
942
  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);
943
+ let connectTimedOut = false;
944
+ let requestTimedOut = false;
945
+ const connectController = connectTimeoutMs && connectTimeoutMs > 0 ? new AbortController() : void 0;
946
+ const requestController = timeoutMs && timeoutMs > 0 ? new AbortController() : void 0;
947
+ const signal = mergeSignals(
948
+ options.signal,
949
+ connectController?.signal,
950
+ requestController?.signal
951
+ );
952
+ const connectTimer = connectController && setTimeout(() => {
953
+ connectTimedOut = true;
954
+ connectController.abort(
955
+ new DOMException("connect timeout", "AbortError")
956
+ );
957
+ }, connectTimeoutMs);
958
+ const requestTimer = requestController && setTimeout(() => {
959
+ requestTimedOut = true;
960
+ requestController.abort(
961
+ new DOMException("timeout", "AbortError")
962
+ );
963
+ }, timeoutMs);
717
964
  try {
718
965
  const response = await fetchFn(url, {
719
966
  method,
@@ -721,6 +968,9 @@ var HTTPClient = class {
721
968
  body: payload,
722
969
  signal
723
970
  });
971
+ if (connectTimer) {
972
+ clearTimeout(connectTimer);
973
+ }
724
974
  if (!response.ok) {
725
975
  const shouldRetry = retryCfg && shouldRetryStatus(
726
976
  response.status,
@@ -728,31 +978,68 @@ var HTTPClient = class {
728
978
  retryCfg.retryPost
729
979
  ) && attempt < attempts;
730
980
  if (shouldRetry) {
981
+ lastStatus = response.status;
731
982
  await backoff(attempt, retryCfg);
732
983
  continue;
733
984
  }
734
- if (!options.raw) {
735
- throw await parseErrorResponse(response);
736
- }
985
+ const retries = buildRetryMetadata(attempt, response.status, lastError);
986
+ const finishedCtx2 = withRequestId(context, response.headers);
987
+ recordHttpMetrics(metrics, trace, start, retries, {
988
+ status: response.status,
989
+ context: finishedCtx2
990
+ });
991
+ throw options.raw ? await parseErrorResponse(response, retries) : await parseErrorResponse(response, retries);
737
992
  }
993
+ const finishedCtx = withRequestId(context, response.headers);
994
+ recordHttpMetrics(metrics, trace, start, void 0, {
995
+ status: response.status,
996
+ context: finishedCtx
997
+ });
738
998
  return response;
739
999
  } catch (err) {
740
1000
  if (options.signal?.aborted) {
741
1001
  throw err;
742
1002
  }
743
- const shouldRetry = retryCfg && isRetryableError(err) && (method !== "POST" || retryCfg.retryPost) && attempt < attempts;
744
- if (!shouldRetry) {
1003
+ if (err instanceof ModelRelayError) {
1004
+ recordHttpMetrics(metrics, trace, start, void 0, {
1005
+ error: err,
1006
+ context
1007
+ });
745
1008
  throw err;
746
1009
  }
1010
+ const transportKind = classifyTransportErrorKind(
1011
+ err,
1012
+ connectTimedOut,
1013
+ requestTimedOut
1014
+ );
1015
+ const shouldRetry = retryCfg && isRetryableError(err, transportKind) && (method !== "POST" || retryCfg.retryPost) && attempt < attempts;
1016
+ if (!shouldRetry) {
1017
+ const retries = buildRetryMetadata(
1018
+ attempt,
1019
+ lastStatus,
1020
+ err instanceof Error ? err.message : String(err)
1021
+ );
1022
+ recordHttpMetrics(metrics, trace, start, retries, {
1023
+ error: err,
1024
+ context
1025
+ });
1026
+ throw toTransportError(err, transportKind, retries);
1027
+ }
747
1028
  lastError = err;
748
1029
  await backoff(attempt, retryCfg);
749
1030
  } finally {
750
- if (timer) {
751
- clearTimeout(timer);
1031
+ if (connectTimer) {
1032
+ clearTimeout(connectTimer);
1033
+ }
1034
+ if (requestTimer) {
1035
+ clearTimeout(requestTimer);
752
1036
  }
753
1037
  }
754
1038
  }
755
- throw lastError instanceof Error ? lastError : new ModelRelayError("request failed", { status: 500 });
1039
+ throw lastError instanceof Error ? lastError : new TransportError("request failed", {
1040
+ kind: "other",
1041
+ retries: buildRetryMetadata(attempts, lastStatus)
1042
+ });
756
1043
  }
757
1044
  async json(path, options = {}) {
758
1045
  const response = await this.request(path, {
@@ -769,7 +1056,7 @@ var HTTPClient = class {
769
1056
  try {
770
1057
  return await response.json();
771
1058
  } catch (err) {
772
- throw new ModelRelayError("failed to parse response JSON", {
1059
+ throw new APIError("failed to parse response JSON", {
773
1060
  status: response.status,
774
1061
  data: err
775
1062
  });
@@ -792,6 +1079,9 @@ function normalizeBaseUrl(value) {
792
1079
  }
793
1080
  return trimmed;
794
1081
  }
1082
+ function isValidHttpUrl(value) {
1083
+ return /^https?:\/\//i.test(value);
1084
+ }
795
1085
  function baseUrlForEnvironment(env) {
796
1086
  if (!env || env === "production") return void 0;
797
1087
  if (env === "staging") return STAGING_BASE_URL;
@@ -817,9 +1107,10 @@ function shouldRetryStatus(status, method, retryPost) {
817
1107
  }
818
1108
  return false;
819
1109
  }
820
- function isRetryableError(err) {
1110
+ function isRetryableError(err, kind) {
821
1111
  if (!err) return false;
822
- return err instanceof DOMException && err.name === "AbortError" || err instanceof TypeError;
1112
+ if (kind === "timeout" || kind === "connect") return true;
1113
+ return err instanceof DOMException || err instanceof TypeError;
823
1114
  }
824
1115
  function backoff(attempt, cfg) {
825
1116
  const exp = Math.max(0, attempt - 1);
@@ -830,24 +1121,22 @@ function backoff(attempt, cfg) {
830
1121
  if (delay <= 0) return Promise.resolve();
831
1122
  return new Promise((resolve) => setTimeout(resolve, delay));
832
1123
  }
833
- function mergeSignals(user, timeoutSignal) {
834
- if (!user && !timeoutSignal) return void 0;
835
- if (user && !timeoutSignal) return user;
836
- if (!user && timeoutSignal) return timeoutSignal;
1124
+ function mergeSignals(...signals) {
1125
+ const active = signals.filter(Boolean);
1126
+ if (active.length === 0) return void 0;
1127
+ if (active.length === 1) return active[0];
837
1128
  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
- );
1129
+ for (const src of active) {
1130
+ if (src.aborted) {
1131
+ controller.abort(src.reason);
1132
+ break;
847
1133
  }
848
- };
849
- if (user) propagate(user);
850
- if (timeoutSignal) propagate(timeoutSignal);
1134
+ src.addEventListener(
1135
+ "abort",
1136
+ () => controller.abort(src.reason),
1137
+ { once: true }
1138
+ );
1139
+ }
851
1140
  return controller.signal;
852
1141
  }
853
1142
  function normalizeHeaders(headers) {
@@ -863,15 +1152,59 @@ function normalizeHeaders(headers) {
863
1152
  }
864
1153
  return normalized;
865
1154
  }
1155
+ function buildRetryMetadata(attempt, lastStatus, lastError) {
1156
+ if (!attempt || attempt <= 1) return void 0;
1157
+ return {
1158
+ attempts: attempt,
1159
+ lastStatus,
1160
+ lastError: typeof lastError === "string" ? lastError : lastError instanceof Error ? lastError.message : lastError ? String(lastError) : void 0
1161
+ };
1162
+ }
1163
+ function classifyTransportErrorKind(err, connectTimedOut, requestTimedOut) {
1164
+ if (connectTimedOut) return "connect";
1165
+ if (requestTimedOut) return "timeout";
1166
+ if (err instanceof DOMException && err.name === "AbortError") {
1167
+ return requestTimedOut ? "timeout" : "request";
1168
+ }
1169
+ if (err instanceof TypeError) return "request";
1170
+ return "other";
1171
+ }
1172
+ function toTransportError(err, kind, retries) {
1173
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : "request failed";
1174
+ return new TransportError(message, { kind, retries, cause: err });
1175
+ }
1176
+ function recordHttpMetrics(metrics, trace, start, retries, info) {
1177
+ if (!metrics?.httpRequest && !trace?.requestFinish) return;
1178
+ const latencyMs = start ? Date.now() - start : 0;
1179
+ if (metrics?.httpRequest) {
1180
+ metrics.httpRequest({
1181
+ latencyMs,
1182
+ status: info.status,
1183
+ error: info.error ? String(info.error) : void 0,
1184
+ retries,
1185
+ context: info.context
1186
+ });
1187
+ }
1188
+ trace?.requestFinish?.({
1189
+ context: info.context,
1190
+ status: info.status,
1191
+ error: info.error,
1192
+ retries,
1193
+ latencyMs
1194
+ });
1195
+ }
1196
+ function withRequestId(context, headers) {
1197
+ const requestId = headers.get("X-ModelRelay-Chat-Request-Id") || headers.get("X-Request-Id") || context.requestId;
1198
+ if (!requestId) return context;
1199
+ return { ...context, requestId };
1200
+ }
866
1201
 
867
1202
  // src/index.ts
868
1203
  var ModelRelay = class {
869
1204
  constructor(options) {
870
1205
  const cfg = options || {};
871
1206
  if (!cfg.key && !cfg.token) {
872
- throw new ModelRelayError("Provide an API key or access token", {
873
- status: 400
874
- });
1207
+ throw new ConfigError("Provide an API key or access token");
875
1208
  }
876
1209
  this.baseUrl = resolveBaseUrl(cfg.environment, cfg.baseUrl);
877
1210
  const http = new HTTPClient({
@@ -880,22 +1213,28 @@ var ModelRelay = class {
880
1213
  accessToken: cfg.token,
881
1214
  fetchImpl: cfg.fetch,
882
1215
  clientHeader: cfg.clientHeader || DEFAULT_CLIENT_HEADER,
1216
+ connectTimeoutMs: cfg.connectTimeoutMs,
883
1217
  timeoutMs: cfg.timeoutMs,
884
1218
  retry: cfg.retry,
885
1219
  defaultHeaders: cfg.defaultHeaders,
886
- environment: cfg.environment
1220
+ environment: cfg.environment,
1221
+ metrics: cfg.metrics,
1222
+ trace: cfg.trace
887
1223
  });
888
1224
  const auth = new AuthClient(http, {
889
1225
  apiKey: cfg.key,
890
1226
  accessToken: cfg.token,
891
- endUser: cfg.endUser
1227
+ customer: cfg.customer
892
1228
  });
893
1229
  this.auth = auth;
894
- this.billing = new BillingClient(http, auth);
895
1230
  this.chat = new ChatClient(http, auth, {
896
- defaultMetadata: cfg.defaultMetadata
1231
+ defaultMetadata: cfg.defaultMetadata,
1232
+ metrics: cfg.metrics,
1233
+ trace: cfg.trace
1234
+ });
1235
+ this.customers = new CustomersClient(http, {
1236
+ apiKey: cfg.key
897
1237
  });
898
- this.apiKeys = new ApiKeysClient(http);
899
1238
  }
900
1239
  };
901
1240
  function resolveBaseUrl(env, override) {
@@ -903,18 +1242,33 @@ function resolveBaseUrl(env, override) {
903
1242
  return base.replace(/\/+$/, "");
904
1243
  }
905
1244
  export {
906
- ApiKeysClient,
1245
+ APIError,
907
1246
  AuthClient,
908
- BillingClient,
909
1247
  ChatClient,
910
1248
  ChatCompletionsStream,
1249
+ ConfigError,
1250
+ CustomersClient,
911
1251
  DEFAULT_BASE_URL,
912
1252
  DEFAULT_CLIENT_HEADER,
1253
+ DEFAULT_CONNECT_TIMEOUT_MS,
913
1254
  DEFAULT_REQUEST_TIMEOUT_MS,
914
1255
  ModelRelay,
915
1256
  ModelRelayError,
1257
+ Models,
1258
+ Providers,
916
1259
  SANDBOX_BASE_URL,
917
1260
  SDK_VERSION,
918
1261
  STAGING_BASE_URL,
919
- isPublishableKey
1262
+ StopReasons,
1263
+ TransportError,
1264
+ isPublishableKey,
1265
+ mergeMetrics,
1266
+ mergeTrace,
1267
+ modelToString,
1268
+ normalizeModelId,
1269
+ normalizeProvider,
1270
+ normalizeStopReason,
1271
+ parseErrorResponse,
1272
+ providerToString,
1273
+ stopReasonToString
920
1274
  };