@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/README.md +108 -26
- package/dist/index.cjs +644 -275
- package/dist/index.d.cts +354 -92
- package/dist/index.d.ts +354 -92
- package/dist/index.js +626 -272
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
123
|
-
if (!
|
|
124
|
-
throw new
|
|
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.
|
|
130
|
-
const ttlSeconds = request?.ttlSeconds ?? this.
|
|
131
|
-
const cacheKey = `${publishableKey}:${
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
211
|
-
tokenType: payload.token_type
|
|
212
|
-
keyId: payload.key_id
|
|
213
|
-
sessionId: payload.session_id
|
|
214
|
-
tokenScope: payload.token_scope
|
|
215
|
-
tokenSource: payload.token_source
|
|
216
|
-
|
|
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
|
-
//
|
|
232
|
-
var
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
};
|
|
344
|
-
|
|
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
|
-
|
|
367
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
534
|
-
const model = p.model || p?.message?.model;
|
|
535
|
-
const stopReason = p.stop_reason
|
|
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
|
|
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
|
-
|
|
601
|
-
inputTokens: Number(payload.input_tokens ??
|
|
602
|
-
outputTokens: Number(payload.output_tokens ??
|
|
603
|
-
totalTokens: Number(payload.total_tokens ??
|
|
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
|
-
//
|
|
649
|
-
var
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
const
|
|
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
|
-
|
|
774
|
-
|
|
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
|
-
|
|
783
|
-
|
|
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 (
|
|
790
|
-
clearTimeout(
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
873
|
-
|
|
874
|
-
if (
|
|
875
|
-
if (
|
|
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
|
|
878
|
-
if (
|
|
879
|
-
controller.abort(
|
|
880
|
-
|
|
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
|
-
|
|
889
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|