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