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