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