@modelrelay/sdk 0.7.0 → 0.17.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 +79 -3
- package/dist/index.cjs +1798 -1455
- package/dist/index.d.cts +147 -39
- package/dist/index.d.ts +147 -39
- package/dist/index.js +1785 -1450
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
// src/errors.ts
|
|
2
|
+
var ErrorCodes = {
|
|
3
|
+
NOT_FOUND: "NOT_FOUND",
|
|
4
|
+
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
5
|
+
RATE_LIMIT: "RATE_LIMIT",
|
|
6
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
7
|
+
FORBIDDEN: "FORBIDDEN",
|
|
8
|
+
CONFLICT: "CONFLICT",
|
|
9
|
+
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
10
|
+
SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
|
|
11
|
+
INVALID_INPUT: "INVALID_INPUT",
|
|
12
|
+
PAYMENT_REQUIRED: "PAYMENT_REQUIRED",
|
|
13
|
+
METHOD_NOT_ALLOWED: "METHOD_NOT_ALLOWED"
|
|
14
|
+
};
|
|
2
15
|
var ModelRelayError = class extends Error {
|
|
3
16
|
constructor(message, opts) {
|
|
4
17
|
super(message);
|
|
@@ -42,6 +55,30 @@ var APIError = class extends ModelRelayError {
|
|
|
42
55
|
retries: opts.retries
|
|
43
56
|
});
|
|
44
57
|
}
|
|
58
|
+
/** Returns true if the error is a not found error. */
|
|
59
|
+
isNotFound() {
|
|
60
|
+
return this.code === ErrorCodes.NOT_FOUND;
|
|
61
|
+
}
|
|
62
|
+
/** Returns true if the error is a validation error. */
|
|
63
|
+
isValidation() {
|
|
64
|
+
return this.code === ErrorCodes.VALIDATION_ERROR || this.code === ErrorCodes.INVALID_INPUT;
|
|
65
|
+
}
|
|
66
|
+
/** Returns true if the error is a rate limit error. */
|
|
67
|
+
isRateLimit() {
|
|
68
|
+
return this.code === ErrorCodes.RATE_LIMIT;
|
|
69
|
+
}
|
|
70
|
+
/** Returns true if the error is an unauthorized error. */
|
|
71
|
+
isUnauthorized() {
|
|
72
|
+
return this.code === ErrorCodes.UNAUTHORIZED;
|
|
73
|
+
}
|
|
74
|
+
/** Returns true if the error is a forbidden error. */
|
|
75
|
+
isForbidden() {
|
|
76
|
+
return this.code === ErrorCodes.FORBIDDEN;
|
|
77
|
+
}
|
|
78
|
+
/** Returns true if the error is a service unavailable error. */
|
|
79
|
+
isUnavailable() {
|
|
80
|
+
return this.code === ErrorCodes.SERVICE_UNAVAILABLE;
|
|
81
|
+
}
|
|
45
82
|
};
|
|
46
83
|
async function parseErrorResponse(response, retries) {
|
|
47
84
|
const requestId = response.headers.get("X-ModelRelay-Chat-Request-Id") || response.headers.get("X-Request-Id") || void 0;
|
|
@@ -96,6 +133,12 @@ async function parseErrorResponse(response, retries) {
|
|
|
96
133
|
}
|
|
97
134
|
|
|
98
135
|
// src/auth.ts
|
|
136
|
+
function createApiKeyAuth(apiKey) {
|
|
137
|
+
return { apiKey };
|
|
138
|
+
}
|
|
139
|
+
function createAccessTokenAuth(accessToken) {
|
|
140
|
+
return { accessToken };
|
|
141
|
+
}
|
|
99
142
|
var AuthClient = class {
|
|
100
143
|
constructor(http, cfg) {
|
|
101
144
|
this.cachedFrontend = /* @__PURE__ */ new Map();
|
|
@@ -155,7 +198,7 @@ var AuthClient = class {
|
|
|
155
198
|
*/
|
|
156
199
|
async authForChat(customerId, overrides) {
|
|
157
200
|
if (this.accessToken) {
|
|
158
|
-
return
|
|
201
|
+
return createAccessTokenAuth(this.accessToken);
|
|
159
202
|
}
|
|
160
203
|
if (!this.apiKey) {
|
|
161
204
|
throw new ConfigError("API key or token is required");
|
|
@@ -166,21 +209,21 @@ var AuthClient = class {
|
|
|
166
209
|
deviceId: overrides?.deviceId,
|
|
167
210
|
ttlSeconds: overrides?.ttlSeconds
|
|
168
211
|
});
|
|
169
|
-
return
|
|
212
|
+
return createAccessTokenAuth(token.token);
|
|
170
213
|
}
|
|
171
|
-
return
|
|
214
|
+
return createApiKeyAuth(this.apiKey);
|
|
172
215
|
}
|
|
173
216
|
/**
|
|
174
217
|
* Billing calls accept either bearer tokens or API keys (including publishable keys).
|
|
175
218
|
*/
|
|
176
219
|
authForBilling() {
|
|
177
220
|
if (this.accessToken) {
|
|
178
|
-
return
|
|
221
|
+
return createAccessTokenAuth(this.accessToken);
|
|
179
222
|
}
|
|
180
223
|
if (!this.apiKey) {
|
|
181
224
|
throw new ConfigError("API key or token is required");
|
|
182
225
|
}
|
|
183
|
-
return
|
|
226
|
+
return createApiKeyAuth(this.apiKey);
|
|
184
227
|
}
|
|
185
228
|
};
|
|
186
229
|
function isPublishableKey(value) {
|
|
@@ -218,7 +261,7 @@ function isTokenReusable(token) {
|
|
|
218
261
|
// package.json
|
|
219
262
|
var package_default = {
|
|
220
263
|
name: "@modelrelay/sdk",
|
|
221
|
-
version: "0.
|
|
264
|
+
version: "0.17.0",
|
|
222
265
|
description: "TypeScript SDK for the ModelRelay API",
|
|
223
266
|
type: "module",
|
|
224
267
|
main: "dist/index.cjs",
|
|
@@ -277,36 +320,34 @@ var StopReasons = {
|
|
|
277
320
|
Incomplete: "incomplete",
|
|
278
321
|
Unknown: "unknown"
|
|
279
322
|
};
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
OpenAIGpt4o: "openai/gpt-4o",
|
|
288
|
-
OpenAIGpt4oMini: "openai/gpt-4o-mini",
|
|
289
|
-
OpenAIGpt51: "openai/gpt-5.1",
|
|
290
|
-
AnthropicClaude35HaikuLatest: "anthropic/claude-3-5-haiku-latest",
|
|
291
|
-
AnthropicClaude35SonnetLatest: "anthropic/claude-3-5-sonnet-latest",
|
|
292
|
-
AnthropicClaudeOpus45: "anthropic/claude-opus-4-5-20251101",
|
|
293
|
-
AnthropicClaude35Haiku: "anthropic/claude-3.5-haiku",
|
|
294
|
-
Grok2: "grok-2",
|
|
295
|
-
Grok4_1FastNonReasoning: "grok-4-1-fast-non-reasoning",
|
|
296
|
-
Grok4_1FastReasoning: "grok-4-1-fast-reasoning",
|
|
297
|
-
Echo1: "echo-1"
|
|
298
|
-
};
|
|
323
|
+
function createUsage(inputTokens, outputTokens, totalTokens) {
|
|
324
|
+
return {
|
|
325
|
+
inputTokens,
|
|
326
|
+
outputTokens,
|
|
327
|
+
totalTokens: totalTokens ?? inputTokens + outputTokens
|
|
328
|
+
};
|
|
329
|
+
}
|
|
299
330
|
var ToolTypes = {
|
|
300
331
|
Function: "function",
|
|
332
|
+
Web: "web",
|
|
301
333
|
WebSearch: "web_search",
|
|
302
334
|
XSearch: "x_search",
|
|
303
335
|
CodeExecution: "code_execution"
|
|
304
336
|
};
|
|
337
|
+
var WebToolModes = {
|
|
338
|
+
Search: "search",
|
|
339
|
+
Browse: "browse"
|
|
340
|
+
};
|
|
305
341
|
var ToolChoiceTypes = {
|
|
306
342
|
Auto: "auto",
|
|
307
343
|
Required: "required",
|
|
308
344
|
None: "none"
|
|
309
345
|
};
|
|
346
|
+
var ResponseFormatTypes = {
|
|
347
|
+
Text: "text",
|
|
348
|
+
JsonObject: "json_object",
|
|
349
|
+
JsonSchema: "json_schema"
|
|
350
|
+
};
|
|
310
351
|
function mergeMetrics(base, override) {
|
|
311
352
|
if (!base && !override) return void 0;
|
|
312
353
|
return {
|
|
@@ -340,1631 +381,1917 @@ function stopReasonToString(value) {
|
|
|
340
381
|
if (typeof value === "string") return value;
|
|
341
382
|
return value.other?.trim() || void 0;
|
|
342
383
|
}
|
|
343
|
-
function normalizeProvider(value) {
|
|
344
|
-
if (value === void 0 || value === null) return void 0;
|
|
345
|
-
const str = String(value).trim();
|
|
346
|
-
if (!str) return void 0;
|
|
347
|
-
const lower = str.toLowerCase();
|
|
348
|
-
for (const p of Object.values(Providers)) {
|
|
349
|
-
if (lower === p) return p;
|
|
350
|
-
}
|
|
351
|
-
return { other: str };
|
|
352
|
-
}
|
|
353
|
-
function providerToString(value) {
|
|
354
|
-
if (!value) return void 0;
|
|
355
|
-
if (typeof value === "string") return value;
|
|
356
|
-
return value.other?.trim() || void 0;
|
|
357
|
-
}
|
|
358
384
|
function normalizeModelId(value) {
|
|
359
385
|
if (value === void 0 || value === null) return void 0;
|
|
360
386
|
const str = String(value).trim();
|
|
361
387
|
if (!str) return void 0;
|
|
362
|
-
|
|
363
|
-
for (const m of Object.values(Models)) {
|
|
364
|
-
if (lower === m) return m;
|
|
365
|
-
}
|
|
366
|
-
return { other: str };
|
|
388
|
+
return str;
|
|
367
389
|
}
|
|
368
390
|
function modelToString(value) {
|
|
369
|
-
|
|
370
|
-
return value.other?.trim() || "";
|
|
391
|
+
return String(value).trim();
|
|
371
392
|
}
|
|
372
393
|
|
|
373
|
-
// src/
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
394
|
+
// src/tools.ts
|
|
395
|
+
function createUserMessage(content) {
|
|
396
|
+
return { role: "user", content };
|
|
397
|
+
}
|
|
398
|
+
function createAssistantMessage(content) {
|
|
399
|
+
return { role: "assistant", content };
|
|
400
|
+
}
|
|
401
|
+
function createSystemMessage(content) {
|
|
402
|
+
return { role: "system", content };
|
|
403
|
+
}
|
|
404
|
+
function createToolCall(id, name, args, type = ToolTypes.Function) {
|
|
405
|
+
return {
|
|
406
|
+
id,
|
|
407
|
+
type,
|
|
408
|
+
function: createFunctionCall(name, args)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function createFunctionCall(name, args) {
|
|
412
|
+
return { name, arguments: args };
|
|
413
|
+
}
|
|
414
|
+
function zodToJsonSchema(schema, options = {}) {
|
|
415
|
+
const result = convertZodType(schema);
|
|
416
|
+
if (options.includeSchema) {
|
|
417
|
+
const schemaVersion = options.target === "draft-04" ? "http://json-schema.org/draft-04/schema#" : options.target === "draft-2019-09" ? "https://json-schema.org/draft/2019-09/schema" : options.target === "draft-2020-12" ? "https://json-schema.org/draft/2020-12/schema" : "http://json-schema.org/draft-07/schema#";
|
|
418
|
+
return { $schema: schemaVersion, ...result };
|
|
393
419
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
};
|
|
425
|
-
const response = await this.http.request("/llm/proxy", {
|
|
426
|
-
method: "POST",
|
|
427
|
-
body,
|
|
428
|
-
headers,
|
|
429
|
-
apiKey: authHeaders.apiKey,
|
|
430
|
-
accessToken: authHeaders.accessToken,
|
|
431
|
-
accept: stream ? "text/event-stream" : "application/json",
|
|
432
|
-
raw: true,
|
|
433
|
-
signal: options.signal,
|
|
434
|
-
timeoutMs: options.timeoutMs ?? (stream ? 0 : void 0),
|
|
435
|
-
useDefaultTimeout: !stream,
|
|
436
|
-
connectTimeoutMs: options.connectTimeoutMs,
|
|
437
|
-
retry: options.retry,
|
|
438
|
-
metrics,
|
|
439
|
-
trace,
|
|
440
|
-
context: baseContext
|
|
441
|
-
});
|
|
442
|
-
const resolvedRequestId = requestIdFromHeaders(response.headers) || requestId || void 0;
|
|
443
|
-
if (!response.ok) {
|
|
444
|
-
throw await parseErrorResponse(response);
|
|
445
|
-
}
|
|
446
|
-
if (!stream) {
|
|
447
|
-
const payload = await response.json();
|
|
448
|
-
const result = normalizeChatResponse(payload, resolvedRequestId);
|
|
449
|
-
if (metrics?.usage) {
|
|
450
|
-
const ctx = {
|
|
451
|
-
...baseContext,
|
|
452
|
-
requestId: resolvedRequestId ?? baseContext.requestId,
|
|
453
|
-
responseId: result.id
|
|
454
|
-
};
|
|
455
|
-
metrics.usage({ usage: result.usage, context: ctx });
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
function convertZodType(schema) {
|
|
423
|
+
const def = schema._def;
|
|
424
|
+
const typeName = def.typeName;
|
|
425
|
+
switch (typeName) {
|
|
426
|
+
case "ZodString":
|
|
427
|
+
return convertZodString(def);
|
|
428
|
+
case "ZodNumber":
|
|
429
|
+
return convertZodNumber(def);
|
|
430
|
+
case "ZodBoolean":
|
|
431
|
+
return { type: "boolean" };
|
|
432
|
+
case "ZodNull":
|
|
433
|
+
return { type: "null" };
|
|
434
|
+
case "ZodArray":
|
|
435
|
+
return convertZodArray(def);
|
|
436
|
+
case "ZodObject":
|
|
437
|
+
return convertZodObject(def);
|
|
438
|
+
case "ZodEnum":
|
|
439
|
+
return convertZodEnum(def);
|
|
440
|
+
case "ZodNativeEnum":
|
|
441
|
+
return convertZodNativeEnum(def);
|
|
442
|
+
case "ZodLiteral":
|
|
443
|
+
return { const: def.value };
|
|
444
|
+
case "ZodUnion":
|
|
445
|
+
return convertZodUnion(def);
|
|
446
|
+
case "ZodOptional": {
|
|
447
|
+
const inner = convertZodType(def.innerType);
|
|
448
|
+
if (def.description && !inner.description) {
|
|
449
|
+
inner.description = def.description;
|
|
456
450
|
}
|
|
457
|
-
return
|
|
451
|
+
return inner;
|
|
458
452
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
453
|
+
case "ZodNullable":
|
|
454
|
+
return convertZodNullable(def);
|
|
455
|
+
case "ZodDefault":
|
|
456
|
+
return { ...convertZodType(def.innerType), default: def.defaultValue() };
|
|
457
|
+
case "ZodEffects":
|
|
458
|
+
return convertZodType(def.schema);
|
|
459
|
+
case "ZodRecord":
|
|
460
|
+
return convertZodRecord(def);
|
|
461
|
+
case "ZodTuple":
|
|
462
|
+
return convertZodTuple(def);
|
|
463
|
+
case "ZodAny":
|
|
464
|
+
case "ZodUnknown":
|
|
465
|
+
return {};
|
|
466
|
+
default:
|
|
467
|
+
return {};
|
|
470
468
|
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
469
|
+
}
|
|
470
|
+
function convertZodString(def) {
|
|
471
|
+
const result = { type: "string" };
|
|
472
|
+
const checks = def.checks;
|
|
473
|
+
if (checks) {
|
|
474
|
+
for (const check of checks) {
|
|
475
|
+
switch (check.kind) {
|
|
476
|
+
case "min":
|
|
477
|
+
result.minLength = check.value;
|
|
478
|
+
break;
|
|
479
|
+
case "max":
|
|
480
|
+
result.maxLength = check.value;
|
|
481
|
+
break;
|
|
482
|
+
case "length":
|
|
483
|
+
result.minLength = check.value;
|
|
484
|
+
result.maxLength = check.value;
|
|
485
|
+
break;
|
|
486
|
+
case "email":
|
|
487
|
+
result.format = "email";
|
|
488
|
+
break;
|
|
489
|
+
case "url":
|
|
490
|
+
result.format = "uri";
|
|
491
|
+
break;
|
|
492
|
+
case "uuid":
|
|
493
|
+
result.format = "uuid";
|
|
494
|
+
break;
|
|
495
|
+
case "datetime":
|
|
496
|
+
result.format = "date-time";
|
|
497
|
+
break;
|
|
498
|
+
case "regex":
|
|
499
|
+
result.pattern = check.value.source;
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
478
502
|
}
|
|
479
|
-
this.response = response;
|
|
480
|
-
this.requestId = requestId;
|
|
481
|
-
this.context = context;
|
|
482
|
-
this.metrics = metrics;
|
|
483
|
-
this.trace = trace;
|
|
484
|
-
this.startedAt = this.metrics?.streamFirstToken || this.trace?.streamEvent || this.trace?.streamError ? Date.now() : 0;
|
|
485
503
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
try {
|
|
489
|
-
await this.response.body?.cancel(reason);
|
|
490
|
-
} catch {
|
|
491
|
-
}
|
|
504
|
+
if (def.description) {
|
|
505
|
+
result.description = def.description;
|
|
492
506
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
const { value, done } = await reader.read();
|
|
511
|
-
if (done) {
|
|
512
|
-
const { events: events2 } = consumeSSEBuffer(buffer, true);
|
|
513
|
-
for (const evt of events2) {
|
|
514
|
-
const parsed = mapChatEvent(evt, this.requestId);
|
|
515
|
-
if (parsed) {
|
|
516
|
-
this.handleStreamEvent(parsed);
|
|
517
|
-
yield parsed;
|
|
518
|
-
}
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
function convertZodNumber(def) {
|
|
510
|
+
const result = { type: "number" };
|
|
511
|
+
const checks = def.checks;
|
|
512
|
+
if (checks) {
|
|
513
|
+
for (const check of checks) {
|
|
514
|
+
switch (check.kind) {
|
|
515
|
+
case "int":
|
|
516
|
+
result.type = "integer";
|
|
517
|
+
break;
|
|
518
|
+
case "min":
|
|
519
|
+
if (check.inclusive === false) {
|
|
520
|
+
result.exclusiveMinimum = check.value;
|
|
521
|
+
} else {
|
|
522
|
+
result.minimum = check.value;
|
|
519
523
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const parsed = mapChatEvent(evt, this.requestId);
|
|
527
|
-
if (parsed) {
|
|
528
|
-
this.handleStreamEvent(parsed);
|
|
529
|
-
yield parsed;
|
|
524
|
+
break;
|
|
525
|
+
case "max":
|
|
526
|
+
if (check.inclusive === false) {
|
|
527
|
+
result.exclusiveMaximum = check.value;
|
|
528
|
+
} else {
|
|
529
|
+
result.maximum = check.value;
|
|
530
530
|
}
|
|
531
|
-
|
|
531
|
+
break;
|
|
532
|
+
case "multipleOf":
|
|
533
|
+
result.multipleOf = check.value;
|
|
534
|
+
break;
|
|
532
535
|
}
|
|
533
|
-
} catch (err) {
|
|
534
|
-
this.recordFirstToken(err);
|
|
535
|
-
this.trace?.streamError?.({ context: this.context, error: err });
|
|
536
|
-
throw err;
|
|
537
|
-
} finally {
|
|
538
|
-
this.closed = true;
|
|
539
|
-
reader.releaseLock();
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
handleStreamEvent(evt) {
|
|
543
|
-
const context = this.enrichContext(evt);
|
|
544
|
-
this.context = context;
|
|
545
|
-
this.trace?.streamEvent?.({ context, event: evt });
|
|
546
|
-
if (evt.type === "message_start" || evt.type === "message_delta" || evt.type === "message_stop" || evt.type === "tool_use_start" || evt.type === "tool_use_delta" || evt.type === "tool_use_stop") {
|
|
547
|
-
this.recordFirstToken();
|
|
548
|
-
}
|
|
549
|
-
if (evt.type === "message_stop" && evt.usage && this.metrics?.usage) {
|
|
550
|
-
this.metrics.usage({ usage: evt.usage, context });
|
|
551
536
|
}
|
|
552
537
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
...this.context,
|
|
556
|
-
responseId: evt.responseId || this.context.responseId,
|
|
557
|
-
requestId: evt.requestId || this.context.requestId,
|
|
558
|
-
model: evt.model || this.context.model
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
recordFirstToken(error) {
|
|
562
|
-
if (!this.metrics?.streamFirstToken || this.firstTokenEmitted) return;
|
|
563
|
-
this.firstTokenEmitted = true;
|
|
564
|
-
const latencyMs = this.startedAt ? Date.now() - this.startedAt : 0;
|
|
565
|
-
this.metrics.streamFirstToken({
|
|
566
|
-
latencyMs,
|
|
567
|
-
error: error ? String(error) : void 0,
|
|
568
|
-
context: this.context
|
|
569
|
-
});
|
|
538
|
+
if (def.description) {
|
|
539
|
+
result.description = def.description;
|
|
570
540
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const lines = buffer.split(/\r?\n/);
|
|
578
|
-
const lastIndex = lines.length - 1;
|
|
579
|
-
const limit = flush ? lines.length : Math.max(0, lastIndex);
|
|
580
|
-
const pushEvent = () => {
|
|
581
|
-
if (!eventName && dataLines.length === 0) {
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
events.push({
|
|
585
|
-
event: eventName || "message",
|
|
586
|
-
data: dataLines.join("\n")
|
|
587
|
-
});
|
|
588
|
-
eventName = "";
|
|
589
|
-
dataLines = [];
|
|
541
|
+
return result;
|
|
542
|
+
}
|
|
543
|
+
function convertZodArray(def) {
|
|
544
|
+
const result = {
|
|
545
|
+
type: "array",
|
|
546
|
+
items: convertZodType(def.type)
|
|
590
547
|
};
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (line === "") {
|
|
594
|
-
pushEvent();
|
|
595
|
-
continue;
|
|
596
|
-
}
|
|
597
|
-
if (line.startsWith(":")) {
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
if (line.startsWith("event:")) {
|
|
601
|
-
eventName = line.slice(6).trim();
|
|
602
|
-
} else if (line.startsWith("data:")) {
|
|
603
|
-
dataLines.push(line.slice(5).trimStart());
|
|
604
|
-
}
|
|
548
|
+
if (def.minLength !== void 0 && def.minLength !== null) {
|
|
549
|
+
result.minItems = def.minLength.value;
|
|
605
550
|
}
|
|
606
|
-
if (
|
|
607
|
-
|
|
608
|
-
remainder = "";
|
|
609
|
-
} else {
|
|
610
|
-
remainder = lines[lastIndex] ?? "";
|
|
551
|
+
if (def.maxLength !== void 0 && def.maxLength !== null) {
|
|
552
|
+
result.maxItems = def.maxLength.value;
|
|
611
553
|
}
|
|
612
|
-
|
|
554
|
+
if (def.description) {
|
|
555
|
+
result.description = def.description;
|
|
556
|
+
}
|
|
557
|
+
return result;
|
|
613
558
|
}
|
|
614
|
-
function
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
559
|
+
function convertZodObject(def) {
|
|
560
|
+
const shape = def.shape;
|
|
561
|
+
const shapeObj = typeof shape === "function" ? shape() : shape;
|
|
562
|
+
const properties = {};
|
|
563
|
+
const required = [];
|
|
564
|
+
for (const [key, value] of Object.entries(shapeObj)) {
|
|
565
|
+
properties[key] = convertZodType(value);
|
|
566
|
+
const valueDef = value._def;
|
|
567
|
+
const isOptional = valueDef.typeName === "ZodOptional" || valueDef.typeName === "ZodDefault" || valueDef.typeName === "ZodNullable" && valueDef.innerType?._def?.typeName === "ZodDefault";
|
|
568
|
+
if (!isOptional) {
|
|
569
|
+
required.push(key);
|
|
621
570
|
}
|
|
622
571
|
}
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const usage = normalizeUsage(p.usage);
|
|
627
|
-
const responseId = p.response_id || p.id || p?.message?.id;
|
|
628
|
-
const model = normalizeModelId(p.model || p?.message?.model);
|
|
629
|
-
const stopReason = normalizeStopReason(p.stop_reason);
|
|
630
|
-
const textDelta = extractTextDelta(p);
|
|
631
|
-
const toolCallDelta = extractToolCallDelta(p, type);
|
|
632
|
-
const toolCalls = extractToolCalls(p, type);
|
|
633
|
-
return {
|
|
634
|
-
type,
|
|
635
|
-
event: raw.event || type,
|
|
636
|
-
data: p,
|
|
637
|
-
textDelta,
|
|
638
|
-
toolCallDelta,
|
|
639
|
-
toolCalls,
|
|
640
|
-
responseId,
|
|
641
|
-
model,
|
|
642
|
-
stopReason,
|
|
643
|
-
usage,
|
|
644
|
-
requestId,
|
|
645
|
-
raw: raw.data || ""
|
|
572
|
+
const result = {
|
|
573
|
+
type: "object",
|
|
574
|
+
properties
|
|
646
575
|
};
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const hint = String(
|
|
650
|
-
payload?.type || payload?.event || eventName || ""
|
|
651
|
-
).trim();
|
|
652
|
-
switch (hint) {
|
|
653
|
-
case "message_start":
|
|
654
|
-
return "message_start";
|
|
655
|
-
case "message_delta":
|
|
656
|
-
return "message_delta";
|
|
657
|
-
case "message_stop":
|
|
658
|
-
return "message_stop";
|
|
659
|
-
case "tool_use_start":
|
|
660
|
-
return "tool_use_start";
|
|
661
|
-
case "tool_use_delta":
|
|
662
|
-
return "tool_use_delta";
|
|
663
|
-
case "tool_use_stop":
|
|
664
|
-
return "tool_use_stop";
|
|
665
|
-
case "ping":
|
|
666
|
-
return "ping";
|
|
667
|
-
default:
|
|
668
|
-
return "custom";
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
function extractTextDelta(payload) {
|
|
672
|
-
if (!payload || typeof payload !== "object") {
|
|
673
|
-
return void 0;
|
|
576
|
+
if (required.length > 0) {
|
|
577
|
+
result.required = required;
|
|
674
578
|
}
|
|
675
|
-
if (
|
|
676
|
-
|
|
579
|
+
if (def.description) {
|
|
580
|
+
result.description = def.description;
|
|
677
581
|
}
|
|
678
|
-
|
|
679
|
-
|
|
582
|
+
const unknownKeys = def.unknownKeys;
|
|
583
|
+
if (unknownKeys === "strict") {
|
|
584
|
+
result.additionalProperties = false;
|
|
680
585
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
function convertZodEnum(def) {
|
|
589
|
+
const result = {
|
|
590
|
+
type: "string",
|
|
591
|
+
enum: def.values
|
|
592
|
+
};
|
|
593
|
+
if (def.description) {
|
|
594
|
+
result.description = def.description;
|
|
688
595
|
}
|
|
689
|
-
return
|
|
596
|
+
return result;
|
|
690
597
|
}
|
|
691
|
-
function
|
|
692
|
-
|
|
693
|
-
|
|
598
|
+
function convertZodNativeEnum(def) {
|
|
599
|
+
const enumValues = def.values;
|
|
600
|
+
const values = Object.values(enumValues).filter(
|
|
601
|
+
(v) => typeof v === "string" || typeof v === "number"
|
|
602
|
+
);
|
|
603
|
+
const result = { enum: values };
|
|
604
|
+
if (def.description) {
|
|
605
|
+
result.description = def.description;
|
|
694
606
|
}
|
|
695
|
-
|
|
696
|
-
|
|
607
|
+
return result;
|
|
608
|
+
}
|
|
609
|
+
function convertZodUnion(def) {
|
|
610
|
+
const options = def.options;
|
|
611
|
+
const result = {
|
|
612
|
+
anyOf: options.map(convertZodType)
|
|
613
|
+
};
|
|
614
|
+
if (def.description) {
|
|
615
|
+
result.description = def.description;
|
|
697
616
|
}
|
|
698
|
-
|
|
699
|
-
const d = payload.tool_call_delta;
|
|
700
|
-
return {
|
|
701
|
-
index: d.index ?? 0,
|
|
702
|
-
id: d.id,
|
|
703
|
-
type: d.type,
|
|
704
|
-
function: d.function ? {
|
|
705
|
-
name: d.function.name,
|
|
706
|
-
arguments: d.function.arguments
|
|
707
|
-
} : void 0
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
if (typeof payload.index === "number" || payload.id || payload.name) {
|
|
711
|
-
return {
|
|
712
|
-
index: payload.index ?? 0,
|
|
713
|
-
id: payload.id,
|
|
714
|
-
type: payload.tool_type,
|
|
715
|
-
function: payload.name || payload.arguments ? {
|
|
716
|
-
name: payload.name,
|
|
717
|
-
arguments: payload.arguments
|
|
718
|
-
} : void 0
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
return void 0;
|
|
617
|
+
return result;
|
|
722
618
|
}
|
|
723
|
-
function
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
619
|
+
function convertZodNullable(def) {
|
|
620
|
+
const inner = convertZodType(def.innerType);
|
|
621
|
+
return {
|
|
622
|
+
anyOf: [inner, { type: "null" }]
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function convertZodRecord(def) {
|
|
626
|
+
const result = {
|
|
627
|
+
type: "object",
|
|
628
|
+
additionalProperties: convertZodType(def.valueType)
|
|
629
|
+
};
|
|
630
|
+
if (def.description) {
|
|
631
|
+
result.description = def.description;
|
|
735
632
|
}
|
|
736
|
-
return
|
|
633
|
+
return result;
|
|
737
634
|
}
|
|
738
|
-
function
|
|
739
|
-
const
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
model: normalizeModelId(p?.model),
|
|
746
|
-
usage: normalizeUsage(p?.usage),
|
|
747
|
-
requestId
|
|
635
|
+
function convertZodTuple(def) {
|
|
636
|
+
const items = def.items;
|
|
637
|
+
const result = {
|
|
638
|
+
type: "array",
|
|
639
|
+
items: items.map(convertZodType),
|
|
640
|
+
minItems: items.length,
|
|
641
|
+
maxItems: items.length
|
|
748
642
|
};
|
|
749
|
-
if (
|
|
750
|
-
|
|
643
|
+
if (def.description) {
|
|
644
|
+
result.description = def.description;
|
|
751
645
|
}
|
|
752
|
-
return
|
|
646
|
+
return result;
|
|
753
647
|
}
|
|
754
|
-
function
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
type: tc.type || ToolTypes.Function,
|
|
758
|
-
function: tc.function ? { name: tc.function.name, arguments: tc.function.arguments } : void 0
|
|
759
|
-
}));
|
|
648
|
+
function createFunctionToolFromSchema(name, description, schema, options) {
|
|
649
|
+
const jsonSchema = zodToJsonSchema(schema, options);
|
|
650
|
+
return createFunctionTool(name, description, jsonSchema);
|
|
760
651
|
}
|
|
761
|
-
function
|
|
762
|
-
|
|
763
|
-
|
|
652
|
+
function createFunctionTool(name, description, parameters) {
|
|
653
|
+
const fn = { name, description };
|
|
654
|
+
if (parameters) {
|
|
655
|
+
fn.parameters = parameters;
|
|
764
656
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
totalTokens: Number(payload.total_tokens ?? 0)
|
|
657
|
+
return {
|
|
658
|
+
type: ToolTypes.Function,
|
|
659
|
+
function: fn
|
|
769
660
|
};
|
|
770
|
-
if (!usage.totalTokens) {
|
|
771
|
-
usage.totalTokens = usage.inputTokens + usage.outputTokens;
|
|
772
|
-
}
|
|
773
|
-
return usage;
|
|
774
661
|
}
|
|
775
|
-
function
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
662
|
+
function createWebTool(options) {
|
|
663
|
+
return {
|
|
664
|
+
type: ToolTypes.Web,
|
|
665
|
+
web: options ? {
|
|
666
|
+
mode: options.mode,
|
|
667
|
+
allowedDomains: options.allowedDomains,
|
|
668
|
+
excludedDomains: options.excludedDomains,
|
|
669
|
+
maxUses: options.maxUses
|
|
670
|
+
} : void 0
|
|
779
671
|
};
|
|
780
|
-
if (typeof params.maxTokens === "number") body.max_tokens = params.maxTokens;
|
|
781
|
-
if (params.provider) body.provider = providerToString(params.provider);
|
|
782
|
-
if (typeof params.temperature === "number")
|
|
783
|
-
body.temperature = params.temperature;
|
|
784
|
-
if (metadata && Object.keys(metadata).length > 0) body.metadata = metadata;
|
|
785
|
-
if (params.stop?.length) body.stop = params.stop;
|
|
786
|
-
if (params.stopSequences?.length) body.stop_sequences = params.stopSequences;
|
|
787
|
-
if (params.tools?.length) body.tools = normalizeTools(params.tools);
|
|
788
|
-
if (params.toolChoice) body.tool_choice = normalizeToolChoice(params.toolChoice);
|
|
789
|
-
return body;
|
|
790
672
|
}
|
|
791
|
-
function
|
|
792
|
-
return
|
|
793
|
-
const normalized = {
|
|
794
|
-
role: msg.role || "user",
|
|
795
|
-
content: msg.content
|
|
796
|
-
};
|
|
797
|
-
if (msg.toolCalls?.length) {
|
|
798
|
-
normalized.tool_calls = msg.toolCalls.map((tc) => ({
|
|
799
|
-
id: tc.id,
|
|
800
|
-
type: tc.type,
|
|
801
|
-
function: tc.function ? { name: tc.function.name, arguments: tc.function.arguments } : void 0
|
|
802
|
-
}));
|
|
803
|
-
}
|
|
804
|
-
if (msg.toolCallId) {
|
|
805
|
-
normalized.tool_call_id = msg.toolCallId;
|
|
806
|
-
}
|
|
807
|
-
return normalized;
|
|
808
|
-
});
|
|
673
|
+
function toolChoiceAuto() {
|
|
674
|
+
return { type: ToolChoiceTypes.Auto };
|
|
809
675
|
}
|
|
810
|
-
function
|
|
811
|
-
return
|
|
812
|
-
const normalized = { type: tool.type };
|
|
813
|
-
if (tool.function) {
|
|
814
|
-
normalized.function = {
|
|
815
|
-
name: tool.function.name,
|
|
816
|
-
description: tool.function.description,
|
|
817
|
-
parameters: tool.function.parameters
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
if (tool.webSearch) {
|
|
821
|
-
normalized.web_search = {
|
|
822
|
-
allowed_domains: tool.webSearch.allowedDomains,
|
|
823
|
-
excluded_domains: tool.webSearch.excludedDomains,
|
|
824
|
-
max_uses: tool.webSearch.maxUses
|
|
825
|
-
};
|
|
826
|
-
}
|
|
827
|
-
if (tool.xSearch) {
|
|
828
|
-
normalized.x_search = {
|
|
829
|
-
allowed_handles: tool.xSearch.allowedHandles,
|
|
830
|
-
excluded_handles: tool.xSearch.excludedHandles,
|
|
831
|
-
from_date: tool.xSearch.fromDate,
|
|
832
|
-
to_date: tool.xSearch.toDate
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
if (tool.codeExecution) {
|
|
836
|
-
normalized.code_execution = {
|
|
837
|
-
language: tool.codeExecution.language,
|
|
838
|
-
timeout_ms: tool.codeExecution.timeoutMs
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
return normalized;
|
|
842
|
-
});
|
|
676
|
+
function toolChoiceRequired() {
|
|
677
|
+
return { type: ToolChoiceTypes.Required };
|
|
843
678
|
}
|
|
844
|
-
function
|
|
845
|
-
return { type:
|
|
679
|
+
function toolChoiceNone() {
|
|
680
|
+
return { type: ToolChoiceTypes.None };
|
|
846
681
|
}
|
|
847
|
-
function
|
|
848
|
-
return
|
|
682
|
+
function hasToolCalls(response) {
|
|
683
|
+
return (response.toolCalls?.length ?? 0) > 0;
|
|
849
684
|
}
|
|
850
|
-
function
|
|
851
|
-
|
|
852
|
-
for (const src of sources) {
|
|
853
|
-
if (!src) continue;
|
|
854
|
-
for (const [key, value] of Object.entries(src)) {
|
|
855
|
-
const k = key?.trim();
|
|
856
|
-
const v = value?.trim();
|
|
857
|
-
if (!k || !v) continue;
|
|
858
|
-
merged[k] = v;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
return Object.keys(merged).length ? merged : void 0;
|
|
685
|
+
function firstToolCall(response) {
|
|
686
|
+
return response.toolCalls?.[0];
|
|
862
687
|
}
|
|
863
|
-
function
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
688
|
+
function toolResultMessage(toolCallId, result) {
|
|
689
|
+
const content = typeof result === "string" ? result : JSON.stringify(result);
|
|
690
|
+
return {
|
|
691
|
+
role: "tool",
|
|
692
|
+
content,
|
|
693
|
+
toolCallId
|
|
694
|
+
};
|
|
867
695
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
696
|
+
function respondToToolCall(call, result) {
|
|
697
|
+
return toolResultMessage(call.id, result);
|
|
698
|
+
}
|
|
699
|
+
function assistantMessageWithToolCalls(content, toolCalls) {
|
|
700
|
+
return {
|
|
701
|
+
role: "assistant",
|
|
702
|
+
content,
|
|
703
|
+
toolCalls
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
var ToolCallAccumulator = class {
|
|
707
|
+
constructor() {
|
|
708
|
+
this.calls = /* @__PURE__ */ new Map();
|
|
881
709
|
}
|
|
882
710
|
/**
|
|
883
|
-
*
|
|
711
|
+
* Processes a streaming tool call delta.
|
|
712
|
+
* Returns true if this started a new tool call.
|
|
884
713
|
*/
|
|
885
|
-
|
|
886
|
-
this.
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
714
|
+
processDelta(delta) {
|
|
715
|
+
const existing = this.calls.get(delta.index);
|
|
716
|
+
if (!existing) {
|
|
717
|
+
this.calls.set(delta.index, {
|
|
718
|
+
id: delta.id ?? "",
|
|
719
|
+
type: delta.type ?? ToolTypes.Function,
|
|
720
|
+
function: {
|
|
721
|
+
name: delta.function?.name ?? "",
|
|
722
|
+
arguments: delta.function?.arguments ?? ""
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
if (delta.function) {
|
|
728
|
+
if (delta.function.name) {
|
|
729
|
+
existing.function = existing.function ?? { name: "", arguments: "" };
|
|
730
|
+
existing.function.name = delta.function.name;
|
|
731
|
+
}
|
|
732
|
+
if (delta.function.arguments) {
|
|
733
|
+
existing.function = existing.function ?? { name: "", arguments: "" };
|
|
734
|
+
existing.function.arguments += delta.function.arguments;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return false;
|
|
892
738
|
}
|
|
893
739
|
/**
|
|
894
|
-
*
|
|
740
|
+
* Returns all accumulated tool calls in index order.
|
|
895
741
|
*/
|
|
896
|
-
|
|
897
|
-
this.
|
|
898
|
-
|
|
899
|
-
throw new ConfigError("tier_id is required");
|
|
900
|
-
}
|
|
901
|
-
if (!request.external_id?.trim()) {
|
|
902
|
-
throw new ConfigError("external_id is required");
|
|
742
|
+
getToolCalls() {
|
|
743
|
+
if (this.calls.size === 0) {
|
|
744
|
+
return [];
|
|
903
745
|
}
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
746
|
+
const maxIdx = Math.max(...this.calls.keys());
|
|
747
|
+
const result = [];
|
|
748
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
749
|
+
const call = this.calls.get(i);
|
|
750
|
+
if (call) {
|
|
751
|
+
result.push(call);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return result;
|
|
910
755
|
}
|
|
911
756
|
/**
|
|
912
|
-
*
|
|
757
|
+
* Returns a specific tool call by index, or undefined if not found.
|
|
913
758
|
*/
|
|
914
|
-
|
|
915
|
-
this.
|
|
916
|
-
if (!customerId?.trim()) {
|
|
917
|
-
throw new ConfigError("customerId is required");
|
|
918
|
-
}
|
|
919
|
-
const response = await this.http.json(
|
|
920
|
-
`/customers/${customerId}`,
|
|
921
|
-
{
|
|
922
|
-
method: "GET",
|
|
923
|
-
apiKey: this.apiKey
|
|
924
|
-
}
|
|
925
|
-
);
|
|
926
|
-
return response.customer;
|
|
759
|
+
getToolCall(index) {
|
|
760
|
+
return this.calls.get(index);
|
|
927
761
|
}
|
|
928
762
|
/**
|
|
929
|
-
*
|
|
930
|
-
* If a customer with the given external_id exists, it is updated.
|
|
931
|
-
* Otherwise, a new customer is created.
|
|
763
|
+
* Clears all accumulated tool calls.
|
|
932
764
|
*/
|
|
933
|
-
|
|
934
|
-
this.
|
|
935
|
-
|
|
936
|
-
|
|
765
|
+
reset() {
|
|
766
|
+
this.calls.clear();
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
var ToolArgsError = class extends Error {
|
|
770
|
+
constructor(message, toolCallId, toolName, rawArguments) {
|
|
771
|
+
super(message);
|
|
772
|
+
this.name = "ToolArgsError";
|
|
773
|
+
this.toolCallId = toolCallId;
|
|
774
|
+
this.toolName = toolName;
|
|
775
|
+
this.rawArguments = rawArguments;
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
function parseToolArgs(call, schema) {
|
|
779
|
+
const toolName = call.function?.name ?? "unknown";
|
|
780
|
+
const rawArgs = call.function?.arguments ?? "";
|
|
781
|
+
let parsed;
|
|
782
|
+
try {
|
|
783
|
+
parsed = rawArgs ? JSON.parse(rawArgs) : {};
|
|
784
|
+
} catch (err) {
|
|
785
|
+
const message = err instanceof Error ? err.message : "Invalid JSON in arguments";
|
|
786
|
+
throw new ToolArgsError(
|
|
787
|
+
`Failed to parse arguments for tool '${toolName}': ${message}`,
|
|
788
|
+
call.id,
|
|
789
|
+
toolName,
|
|
790
|
+
rawArgs
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
return schema.parse(parsed);
|
|
795
|
+
} catch (err) {
|
|
796
|
+
let message;
|
|
797
|
+
if (err instanceof Error) {
|
|
798
|
+
const zodErr = err;
|
|
799
|
+
if (zodErr.errors && Array.isArray(zodErr.errors)) {
|
|
800
|
+
const issues = zodErr.errors.map((e) => {
|
|
801
|
+
const path = e.path.length > 0 ? `${e.path.join(".")}: ` : "";
|
|
802
|
+
return `${path}${e.message}`;
|
|
803
|
+
}).join("; ");
|
|
804
|
+
message = issues;
|
|
805
|
+
} else {
|
|
806
|
+
message = err.message;
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
message = String(err);
|
|
937
810
|
}
|
|
938
|
-
|
|
939
|
-
|
|
811
|
+
throw new ToolArgsError(
|
|
812
|
+
`Invalid arguments for tool '${toolName}': ${message}`,
|
|
813
|
+
call.id,
|
|
814
|
+
toolName,
|
|
815
|
+
rawArgs
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function tryParseToolArgs(call, schema) {
|
|
820
|
+
try {
|
|
821
|
+
const data = parseToolArgs(call, schema);
|
|
822
|
+
return { success: true, data };
|
|
823
|
+
} catch (err) {
|
|
824
|
+
if (err instanceof ToolArgsError) {
|
|
825
|
+
return { success: false, error: err };
|
|
940
826
|
}
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
827
|
+
const toolName = call.function?.name ?? "unknown";
|
|
828
|
+
const rawArgs = call.function?.arguments ?? "";
|
|
829
|
+
return {
|
|
830
|
+
success: false,
|
|
831
|
+
error: new ToolArgsError(
|
|
832
|
+
err instanceof Error ? err.message : String(err),
|
|
833
|
+
call.id,
|
|
834
|
+
toolName,
|
|
835
|
+
rawArgs
|
|
836
|
+
)
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function parseToolArgsRaw(call) {
|
|
841
|
+
const toolName = call.function?.name ?? "unknown";
|
|
842
|
+
const rawArgs = call.function?.arguments ?? "";
|
|
843
|
+
try {
|
|
844
|
+
return rawArgs ? JSON.parse(rawArgs) : {};
|
|
845
|
+
} catch (err) {
|
|
846
|
+
const message = err instanceof Error ? err.message : "Invalid JSON in arguments";
|
|
847
|
+
throw new ToolArgsError(
|
|
848
|
+
`Failed to parse arguments for tool '${toolName}': ${message}`,
|
|
849
|
+
call.id,
|
|
850
|
+
toolName,
|
|
851
|
+
rawArgs
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
var ToolRegistry = class {
|
|
856
|
+
constructor() {
|
|
857
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
947
858
|
}
|
|
948
859
|
/**
|
|
949
|
-
*
|
|
860
|
+
* Registers a handler function for a tool name.
|
|
861
|
+
* @param name - The tool name (must match the function name in the tool definition)
|
|
862
|
+
* @param handler - Function to execute when this tool is called
|
|
863
|
+
* @returns this for chaining
|
|
950
864
|
*/
|
|
951
|
-
|
|
952
|
-
this.
|
|
953
|
-
|
|
954
|
-
throw new ConfigError("customerId is required");
|
|
955
|
-
}
|
|
956
|
-
await this.http.request(`/customers/${customerId}`, {
|
|
957
|
-
method: "DELETE",
|
|
958
|
-
apiKey: this.apiKey
|
|
959
|
-
});
|
|
865
|
+
register(name, handler) {
|
|
866
|
+
this.handlers.set(name, handler);
|
|
867
|
+
return this;
|
|
960
868
|
}
|
|
961
869
|
/**
|
|
962
|
-
*
|
|
870
|
+
* Unregisters a tool handler.
|
|
871
|
+
* @param name - The tool name to unregister
|
|
872
|
+
* @returns true if the handler was removed, false if it didn't exist
|
|
963
873
|
*/
|
|
964
|
-
|
|
965
|
-
this.
|
|
966
|
-
if (!customerId?.trim()) {
|
|
967
|
-
throw new ConfigError("customerId is required");
|
|
968
|
-
}
|
|
969
|
-
if (!request.success_url?.trim() || !request.cancel_url?.trim()) {
|
|
970
|
-
throw new ConfigError("success_url and cancel_url are required");
|
|
971
|
-
}
|
|
972
|
-
return await this.http.json(
|
|
973
|
-
`/customers/${customerId}/checkout`,
|
|
974
|
-
{
|
|
975
|
-
method: "POST",
|
|
976
|
-
body: request,
|
|
977
|
-
apiKey: this.apiKey
|
|
978
|
-
}
|
|
979
|
-
);
|
|
874
|
+
unregister(name) {
|
|
875
|
+
return this.handlers.delete(name);
|
|
980
876
|
}
|
|
981
877
|
/**
|
|
982
|
-
*
|
|
878
|
+
* Checks if a handler is registered for the given tool name.
|
|
983
879
|
*/
|
|
984
|
-
|
|
985
|
-
this.
|
|
986
|
-
if (!customerId?.trim()) {
|
|
987
|
-
throw new ConfigError("customerId is required");
|
|
988
|
-
}
|
|
989
|
-
return await this.http.json(
|
|
990
|
-
`/customers/${customerId}/subscription`,
|
|
991
|
-
{
|
|
992
|
-
method: "GET",
|
|
993
|
-
apiKey: this.apiKey
|
|
994
|
-
}
|
|
995
|
-
);
|
|
996
|
-
}
|
|
997
|
-
};
|
|
998
|
-
|
|
999
|
-
// src/tiers.ts
|
|
1000
|
-
var TiersClient = class {
|
|
1001
|
-
constructor(http, cfg) {
|
|
1002
|
-
this.http = http;
|
|
1003
|
-
this.apiKey = cfg.apiKey;
|
|
1004
|
-
}
|
|
1005
|
-
ensureApiKey() {
|
|
1006
|
-
if (!this.apiKey || !this.apiKey.startsWith("mr_pk_") && !this.apiKey.startsWith("mr_sk_")) {
|
|
1007
|
-
throw new ConfigError(
|
|
1008
|
-
"API key (mr_pk_* or mr_sk_*) required for tier operations"
|
|
1009
|
-
);
|
|
1010
|
-
}
|
|
880
|
+
has(name) {
|
|
881
|
+
return this.handlers.has(name);
|
|
1011
882
|
}
|
|
1012
883
|
/**
|
|
1013
|
-
*
|
|
884
|
+
* Returns the list of registered tool names.
|
|
1014
885
|
*/
|
|
1015
|
-
|
|
1016
|
-
this.
|
|
1017
|
-
const response = await this.http.json("/tiers", {
|
|
1018
|
-
method: "GET",
|
|
1019
|
-
apiKey: this.apiKey
|
|
1020
|
-
});
|
|
1021
|
-
return response.tiers;
|
|
886
|
+
getRegisteredTools() {
|
|
887
|
+
return Array.from(this.handlers.keys());
|
|
1022
888
|
}
|
|
1023
889
|
/**
|
|
1024
|
-
*
|
|
890
|
+
* Executes a single tool call.
|
|
891
|
+
* @param call - The tool call to execute
|
|
892
|
+
* @returns The execution result
|
|
1025
893
|
*/
|
|
1026
|
-
async
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
894
|
+
async execute(call) {
|
|
895
|
+
const toolName = call.function?.name ?? "";
|
|
896
|
+
const handler = this.handlers.get(toolName);
|
|
897
|
+
if (!handler) {
|
|
898
|
+
return {
|
|
899
|
+
toolCallId: call.id,
|
|
900
|
+
toolName,
|
|
901
|
+
result: null,
|
|
902
|
+
error: `Unknown tool: '${toolName}'. Available tools: ${this.getRegisteredTools().join(", ") || "none"}`
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
let args;
|
|
906
|
+
try {
|
|
907
|
+
args = call.function?.arguments ? JSON.parse(call.function.arguments) : {};
|
|
908
|
+
} catch (err) {
|
|
909
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
910
|
+
return {
|
|
911
|
+
toolCallId: call.id,
|
|
912
|
+
toolName,
|
|
913
|
+
result: null,
|
|
914
|
+
error: `Invalid JSON in arguments: ${errorMessage}`,
|
|
915
|
+
isRetryable: true
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
try {
|
|
919
|
+
const result = await handler(args, call);
|
|
920
|
+
return {
|
|
921
|
+
toolCallId: call.id,
|
|
922
|
+
toolName,
|
|
923
|
+
result
|
|
924
|
+
};
|
|
925
|
+
} catch (err) {
|
|
926
|
+
const isRetryable = err instanceof ToolArgsError;
|
|
927
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
928
|
+
return {
|
|
929
|
+
toolCallId: call.id,
|
|
930
|
+
toolName,
|
|
931
|
+
result: null,
|
|
932
|
+
error: errorMessage,
|
|
933
|
+
isRetryable
|
|
934
|
+
};
|
|
1047
935
|
}
|
|
1048
|
-
this.baseUrl = resolvedBase;
|
|
1049
|
-
this.apiKey = cfg.apiKey?.trim();
|
|
1050
|
-
this.accessToken = cfg.accessToken?.trim();
|
|
1051
|
-
this.fetchImpl = cfg.fetchImpl;
|
|
1052
|
-
this.clientHeader = cfg.clientHeader?.trim() || DEFAULT_CLIENT_HEADER;
|
|
1053
|
-
this.defaultConnectTimeoutMs = cfg.connectTimeoutMs === void 0 ? DEFAULT_CONNECT_TIMEOUT_MS : Math.max(0, cfg.connectTimeoutMs);
|
|
1054
|
-
this.defaultTimeoutMs = cfg.timeoutMs === void 0 ? DEFAULT_REQUEST_TIMEOUT_MS : Math.max(0, cfg.timeoutMs);
|
|
1055
|
-
this.retry = normalizeRetryConfig(cfg.retry);
|
|
1056
|
-
this.defaultHeaders = normalizeHeaders(cfg.defaultHeaders);
|
|
1057
|
-
this.metrics = cfg.metrics;
|
|
1058
|
-
this.trace = cfg.trace;
|
|
1059
936
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
937
|
+
/**
|
|
938
|
+
* Executes multiple tool calls in parallel.
|
|
939
|
+
* @param calls - Array of tool calls to execute
|
|
940
|
+
* @returns Array of execution results in the same order as input
|
|
941
|
+
*/
|
|
942
|
+
async executeAll(calls) {
|
|
943
|
+
return Promise.all(calls.map((call) => this.execute(call)));
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Converts execution results to tool result messages.
|
|
947
|
+
* Useful for appending to the conversation history.
|
|
948
|
+
* @param results - Array of execution results
|
|
949
|
+
* @returns Array of ChatMessage objects with role "tool"
|
|
950
|
+
*/
|
|
951
|
+
resultsToMessages(results) {
|
|
952
|
+
return results.map((r) => {
|
|
953
|
+
const content = r.error ? `Error: ${r.error}` : typeof r.result === "string" ? r.result : JSON.stringify(r.result);
|
|
954
|
+
return toolResultMessage(r.toolCallId, content);
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
function formatToolErrorForModel(result) {
|
|
959
|
+
const lines = [
|
|
960
|
+
`Tool call error for '${result.toolName}': ${result.error}`
|
|
961
|
+
];
|
|
962
|
+
if (result.isRetryable) {
|
|
963
|
+
lines.push("");
|
|
964
|
+
lines.push("Please correct the arguments and try again.");
|
|
965
|
+
}
|
|
966
|
+
return lines.join("\n");
|
|
967
|
+
}
|
|
968
|
+
function hasRetryableErrors(results) {
|
|
969
|
+
return results.some((r) => r.error && r.isRetryable);
|
|
970
|
+
}
|
|
971
|
+
function getRetryableErrors(results) {
|
|
972
|
+
return results.filter((r) => r.error && r.isRetryable);
|
|
973
|
+
}
|
|
974
|
+
function createRetryMessages(results) {
|
|
975
|
+
return results.filter((r) => r.error && r.isRetryable).map((r) => toolResultMessage(r.toolCallId, formatToolErrorForModel(r)));
|
|
976
|
+
}
|
|
977
|
+
async function executeWithRetry(registry, toolCalls, options = {}) {
|
|
978
|
+
const maxRetries = options.maxRetries ?? 2;
|
|
979
|
+
let currentCalls = toolCalls;
|
|
980
|
+
let attempt = 0;
|
|
981
|
+
const successfulResults = /* @__PURE__ */ new Map();
|
|
982
|
+
while (attempt <= maxRetries) {
|
|
983
|
+
const results = await registry.executeAll(currentCalls);
|
|
984
|
+
for (const result of results) {
|
|
985
|
+
if (!result.error || !result.isRetryable) {
|
|
986
|
+
successfulResults.set(result.toolCallId, result);
|
|
987
|
+
}
|
|
1066
988
|
}
|
|
1067
|
-
const
|
|
1068
|
-
|
|
989
|
+
const retryableResults = getRetryableErrors(results);
|
|
990
|
+
if (retryableResults.length === 0 || !options.onRetry) {
|
|
991
|
+
for (const result of results) {
|
|
992
|
+
if (result.error && result.isRetryable) {
|
|
993
|
+
successfulResults.set(result.toolCallId, result);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return Array.from(successfulResults.values());
|
|
997
|
+
}
|
|
998
|
+
attempt++;
|
|
999
|
+
if (attempt > maxRetries) {
|
|
1000
|
+
for (const result of retryableResults) {
|
|
1001
|
+
successfulResults.set(result.toolCallId, result);
|
|
1002
|
+
}
|
|
1003
|
+
return Array.from(successfulResults.values());
|
|
1004
|
+
}
|
|
1005
|
+
const errorMessages = createRetryMessages(retryableResults);
|
|
1006
|
+
const newCalls = await options.onRetry(errorMessages, attempt);
|
|
1007
|
+
if (newCalls.length === 0) {
|
|
1008
|
+
for (const result of retryableResults) {
|
|
1009
|
+
successfulResults.set(result.toolCallId, result);
|
|
1010
|
+
}
|
|
1011
|
+
return Array.from(successfulResults.values());
|
|
1012
|
+
}
|
|
1013
|
+
currentCalls = newCalls;
|
|
1014
|
+
}
|
|
1015
|
+
return Array.from(successfulResults.values());
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// src/chat.ts
|
|
1019
|
+
var REQUEST_ID_HEADER = "X-ModelRelay-Chat-Request-Id";
|
|
1020
|
+
var ChatClient = class {
|
|
1021
|
+
constructor(http, auth, cfg = {}) {
|
|
1022
|
+
this.completions = new ChatCompletionsClient(
|
|
1023
|
+
http,
|
|
1024
|
+
auth,
|
|
1025
|
+
cfg.defaultMetadata,
|
|
1026
|
+
cfg.metrics,
|
|
1027
|
+
cfg.trace
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
var ChatCompletionsClient = class {
|
|
1032
|
+
constructor(http, auth, defaultMetadata, metrics, trace) {
|
|
1033
|
+
this.http = http;
|
|
1034
|
+
this.auth = auth;
|
|
1035
|
+
this.defaultMetadata = defaultMetadata;
|
|
1036
|
+
this.metrics = metrics;
|
|
1037
|
+
this.trace = trace;
|
|
1038
|
+
}
|
|
1039
|
+
async create(params, options = {}) {
|
|
1040
|
+
const stream = options.stream ?? params.stream ?? true;
|
|
1069
1041
|
const metrics = mergeMetrics(this.metrics, options.metrics);
|
|
1070
1042
|
const trace = mergeTrace(this.trace, options.trace);
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1043
|
+
if (!params?.messages?.length) {
|
|
1044
|
+
throw new ConfigError("at least one message is required");
|
|
1045
|
+
}
|
|
1046
|
+
if (!hasUserMessage(params.messages)) {
|
|
1047
|
+
throw new ConfigError("at least one user message is required");
|
|
1048
|
+
}
|
|
1049
|
+
const authHeaders = await this.auth.authForChat(params.customerId);
|
|
1050
|
+
const body = buildProxyBody(
|
|
1051
|
+
params,
|
|
1052
|
+
mergeMetadata(this.defaultMetadata, params.metadata, options.metadata)
|
|
1053
|
+
);
|
|
1054
|
+
const requestId = params.requestId || options.requestId;
|
|
1055
|
+
const headers = { ...options.headers || {} };
|
|
1056
|
+
if (requestId) {
|
|
1057
|
+
headers[REQUEST_ID_HEADER] = requestId;
|
|
1058
|
+
}
|
|
1059
|
+
const baseContext = {
|
|
1060
|
+
method: "POST",
|
|
1061
|
+
path: "/llm/proxy",
|
|
1062
|
+
model: params.model,
|
|
1063
|
+
requestId
|
|
1075
1064
|
};
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1065
|
+
const response = await this.http.request("/llm/proxy", {
|
|
1066
|
+
method: "POST",
|
|
1067
|
+
body,
|
|
1068
|
+
headers,
|
|
1069
|
+
apiKey: authHeaders.apiKey,
|
|
1070
|
+
accessToken: authHeaders.accessToken,
|
|
1071
|
+
accept: stream ? "text/event-stream" : "application/json",
|
|
1072
|
+
raw: true,
|
|
1073
|
+
signal: options.signal,
|
|
1074
|
+
timeoutMs: options.timeoutMs ?? (stream ? 0 : void 0),
|
|
1075
|
+
useDefaultTimeout: !stream,
|
|
1076
|
+
connectTimeoutMs: options.connectTimeoutMs,
|
|
1077
|
+
retry: options.retry,
|
|
1078
|
+
metrics,
|
|
1079
|
+
trace,
|
|
1080
|
+
context: baseContext
|
|
1081
1081
|
});
|
|
1082
|
-
const
|
|
1083
|
-
if (
|
|
1084
|
-
|
|
1082
|
+
const resolvedRequestId = requestIdFromHeaders(response.headers) || requestId || void 0;
|
|
1083
|
+
if (!response.ok) {
|
|
1084
|
+
throw await parseErrorResponse(response);
|
|
1085
1085
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1086
|
+
if (!stream) {
|
|
1087
|
+
const payload = await response.json();
|
|
1088
|
+
const result = normalizeChatResponse(payload, resolvedRequestId);
|
|
1089
|
+
if (metrics?.usage) {
|
|
1090
|
+
const ctx = {
|
|
1091
|
+
...baseContext,
|
|
1092
|
+
requestId: resolvedRequestId ?? baseContext.requestId,
|
|
1093
|
+
responseId: result.id
|
|
1094
|
+
};
|
|
1095
|
+
metrics.usage({ usage: result.usage, context: ctx });
|
|
1096
|
+
}
|
|
1097
|
+
return result;
|
|
1091
1098
|
}
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1099
|
+
const streamContext = {
|
|
1100
|
+
...baseContext,
|
|
1101
|
+
requestId: resolvedRequestId ?? baseContext.requestId
|
|
1102
|
+
};
|
|
1103
|
+
return new ChatCompletionsStream(
|
|
1104
|
+
response,
|
|
1105
|
+
resolvedRequestId,
|
|
1106
|
+
streamContext,
|
|
1107
|
+
metrics,
|
|
1108
|
+
trace
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Stream structured JSON responses using the NDJSON contract defined for
|
|
1113
|
+
* /llm/proxy. The request must include a structured responseFormat.
|
|
1114
|
+
*/
|
|
1115
|
+
async streamJSON(params, options = {}) {
|
|
1116
|
+
const metrics = mergeMetrics(this.metrics, options.metrics);
|
|
1117
|
+
const trace = mergeTrace(this.trace, options.trace);
|
|
1118
|
+
if (!params?.messages?.length) {
|
|
1119
|
+
throw new ConfigError("at least one message is required");
|
|
1096
1120
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
headers.set("X-ModelRelay-Api-Key", apiKey);
|
|
1121
|
+
if (!hasUserMessage(params.messages)) {
|
|
1122
|
+
throw new ConfigError("at least one user message is required");
|
|
1100
1123
|
}
|
|
1101
|
-
if (
|
|
1102
|
-
|
|
1124
|
+
if (!params.responseFormat || params.responseFormat.type !== "json_object" && params.responseFormat.type !== "json_schema") {
|
|
1125
|
+
throw new ConfigError(
|
|
1126
|
+
"responseFormat with type=json_object or json_schema is required for structured streaming"
|
|
1127
|
+
);
|
|
1103
1128
|
}
|
|
1104
|
-
const
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1129
|
+
const authHeaders = await this.auth.authForChat(params.customerId);
|
|
1130
|
+
const body = buildProxyBody(
|
|
1131
|
+
params,
|
|
1132
|
+
mergeMetadata(this.defaultMetadata, params.metadata, options.metadata)
|
|
1108
1133
|
);
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
let connectTimedOut = false;
|
|
1114
|
-
let requestTimedOut = false;
|
|
1115
|
-
const connectController = connectTimeoutMs && connectTimeoutMs > 0 ? new AbortController() : void 0;
|
|
1116
|
-
const requestController = timeoutMs && timeoutMs > 0 ? new AbortController() : void 0;
|
|
1117
|
-
const signal = mergeSignals(
|
|
1118
|
-
options.signal,
|
|
1119
|
-
connectController?.signal,
|
|
1120
|
-
requestController?.signal
|
|
1121
|
-
);
|
|
1122
|
-
const connectTimer = connectController && setTimeout(() => {
|
|
1123
|
-
connectTimedOut = true;
|
|
1124
|
-
connectController.abort(
|
|
1125
|
-
new DOMException("connect timeout", "AbortError")
|
|
1126
|
-
);
|
|
1127
|
-
}, connectTimeoutMs);
|
|
1128
|
-
const requestTimer = requestController && setTimeout(() => {
|
|
1129
|
-
requestTimedOut = true;
|
|
1130
|
-
requestController.abort(
|
|
1131
|
-
new DOMException("timeout", "AbortError")
|
|
1132
|
-
);
|
|
1133
|
-
}, timeoutMs);
|
|
1134
|
-
try {
|
|
1135
|
-
const response = await fetchFn(url, {
|
|
1136
|
-
method,
|
|
1137
|
-
headers,
|
|
1138
|
-
body: payload,
|
|
1139
|
-
signal
|
|
1140
|
-
});
|
|
1141
|
-
if (connectTimer) {
|
|
1142
|
-
clearTimeout(connectTimer);
|
|
1143
|
-
}
|
|
1144
|
-
if (!response.ok) {
|
|
1145
|
-
const shouldRetry = retryCfg && shouldRetryStatus(
|
|
1146
|
-
response.status,
|
|
1147
|
-
method,
|
|
1148
|
-
retryCfg.retryPost
|
|
1149
|
-
) && attempt < attempts;
|
|
1150
|
-
if (shouldRetry) {
|
|
1151
|
-
lastStatus = response.status;
|
|
1152
|
-
await backoff(attempt, retryCfg);
|
|
1153
|
-
continue;
|
|
1154
|
-
}
|
|
1155
|
-
const retries = buildRetryMetadata(attempt, response.status, lastError);
|
|
1156
|
-
const finishedCtx2 = withRequestId(context, response.headers);
|
|
1157
|
-
recordHttpMetrics(metrics, trace, start, retries, {
|
|
1158
|
-
status: response.status,
|
|
1159
|
-
context: finishedCtx2
|
|
1160
|
-
});
|
|
1161
|
-
throw options.raw ? await parseErrorResponse(response, retries) : await parseErrorResponse(response, retries);
|
|
1162
|
-
}
|
|
1163
|
-
const finishedCtx = withRequestId(context, response.headers);
|
|
1164
|
-
recordHttpMetrics(metrics, trace, start, void 0, {
|
|
1165
|
-
status: response.status,
|
|
1166
|
-
context: finishedCtx
|
|
1167
|
-
});
|
|
1168
|
-
return response;
|
|
1169
|
-
} catch (err) {
|
|
1170
|
-
if (options.signal?.aborted) {
|
|
1171
|
-
throw err;
|
|
1172
|
-
}
|
|
1173
|
-
if (err instanceof ModelRelayError) {
|
|
1174
|
-
recordHttpMetrics(metrics, trace, start, void 0, {
|
|
1175
|
-
error: err,
|
|
1176
|
-
context
|
|
1177
|
-
});
|
|
1178
|
-
throw err;
|
|
1179
|
-
}
|
|
1180
|
-
const transportKind = classifyTransportErrorKind(
|
|
1181
|
-
err,
|
|
1182
|
-
connectTimedOut,
|
|
1183
|
-
requestTimedOut
|
|
1184
|
-
);
|
|
1185
|
-
const shouldRetry = retryCfg && isRetryableError(err, transportKind) && (method !== "POST" || retryCfg.retryPost) && attempt < attempts;
|
|
1186
|
-
if (!shouldRetry) {
|
|
1187
|
-
const retries = buildRetryMetadata(
|
|
1188
|
-
attempt,
|
|
1189
|
-
lastStatus,
|
|
1190
|
-
err instanceof Error ? err.message : String(err)
|
|
1191
|
-
);
|
|
1192
|
-
recordHttpMetrics(metrics, trace, start, retries, {
|
|
1193
|
-
error: err,
|
|
1194
|
-
context
|
|
1195
|
-
});
|
|
1196
|
-
throw toTransportError(err, transportKind, retries);
|
|
1197
|
-
}
|
|
1198
|
-
lastError = err;
|
|
1199
|
-
await backoff(attempt, retryCfg);
|
|
1200
|
-
} finally {
|
|
1201
|
-
if (connectTimer) {
|
|
1202
|
-
clearTimeout(connectTimer);
|
|
1203
|
-
}
|
|
1204
|
-
if (requestTimer) {
|
|
1205
|
-
clearTimeout(requestTimer);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1134
|
+
const requestId = params.requestId || options.requestId;
|
|
1135
|
+
const headers = { ...options.headers || {} };
|
|
1136
|
+
if (requestId) {
|
|
1137
|
+
headers[REQUEST_ID_HEADER] = requestId;
|
|
1208
1138
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
const response = await this.request(
|
|
1216
|
-
|
|
1139
|
+
const baseContext = {
|
|
1140
|
+
method: "POST",
|
|
1141
|
+
path: "/llm/proxy",
|
|
1142
|
+
model: params.model,
|
|
1143
|
+
requestId
|
|
1144
|
+
};
|
|
1145
|
+
const response = await this.http.request("/llm/proxy", {
|
|
1146
|
+
method: "POST",
|
|
1147
|
+
body,
|
|
1148
|
+
headers,
|
|
1149
|
+
apiKey: authHeaders.apiKey,
|
|
1150
|
+
accessToken: authHeaders.accessToken,
|
|
1151
|
+
accept: "application/x-ndjson",
|
|
1217
1152
|
raw: true,
|
|
1218
|
-
|
|
1153
|
+
signal: options.signal,
|
|
1154
|
+
timeoutMs: options.timeoutMs ?? 0,
|
|
1155
|
+
useDefaultTimeout: false,
|
|
1156
|
+
connectTimeoutMs: options.connectTimeoutMs,
|
|
1157
|
+
retry: options.retry,
|
|
1158
|
+
metrics,
|
|
1159
|
+
trace,
|
|
1160
|
+
context: baseContext
|
|
1219
1161
|
});
|
|
1162
|
+
const resolvedRequestId = requestIdFromHeaders(response.headers) || requestId || void 0;
|
|
1220
1163
|
if (!response.ok) {
|
|
1221
1164
|
throw await parseErrorResponse(response);
|
|
1222
1165
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
throw new APIError("failed to parse response JSON", {
|
|
1230
|
-
status: response.status,
|
|
1231
|
-
data: err
|
|
1232
|
-
});
|
|
1166
|
+
const contentType = response.headers.get("Content-Type") || "";
|
|
1167
|
+
if (!/application\/(x-)?ndjson/i.test(contentType)) {
|
|
1168
|
+
throw new TransportError(
|
|
1169
|
+
`expected NDJSON structured stream, got Content-Type ${contentType || "missing"}`,
|
|
1170
|
+
{ kind: "request" }
|
|
1171
|
+
);
|
|
1233
1172
|
}
|
|
1173
|
+
const streamContext = {
|
|
1174
|
+
...baseContext,
|
|
1175
|
+
requestId: resolvedRequestId ?? baseContext.requestId
|
|
1176
|
+
};
|
|
1177
|
+
return new StructuredJSONStream(
|
|
1178
|
+
response,
|
|
1179
|
+
resolvedRequestId,
|
|
1180
|
+
streamContext,
|
|
1181
|
+
metrics,
|
|
1182
|
+
trace
|
|
1183
|
+
);
|
|
1234
1184
|
}
|
|
1235
1185
|
};
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
}
|
|
1250
|
-
return trimmed;
|
|
1251
|
-
}
|
|
1252
|
-
function isValidHttpUrl(value) {
|
|
1253
|
-
return /^https?:\/\//i.test(value);
|
|
1254
|
-
}
|
|
1255
|
-
function normalizeRetryConfig(retry) {
|
|
1256
|
-
if (retry === false) return void 0;
|
|
1257
|
-
const cfg = retry || {};
|
|
1258
|
-
return {
|
|
1259
|
-
maxAttempts: Math.max(1, cfg.maxAttempts ?? 3),
|
|
1260
|
-
baseBackoffMs: Math.max(0, cfg.baseBackoffMs ?? 300),
|
|
1261
|
-
maxBackoffMs: Math.max(0, cfg.maxBackoffMs ?? 5e3),
|
|
1262
|
-
retryPost: cfg.retryPost ?? true
|
|
1263
|
-
};
|
|
1264
|
-
}
|
|
1265
|
-
function shouldRetryStatus(status, method, retryPost) {
|
|
1266
|
-
if (status === 408 || status === 429) {
|
|
1267
|
-
return method !== "POST" || retryPost;
|
|
1186
|
+
var ChatCompletionsStream = class {
|
|
1187
|
+
constructor(response, requestId, context, metrics, trace) {
|
|
1188
|
+
this.firstTokenEmitted = false;
|
|
1189
|
+
this.closed = false;
|
|
1190
|
+
if (!response.body) {
|
|
1191
|
+
throw new ConfigError("streaming response is missing a body");
|
|
1192
|
+
}
|
|
1193
|
+
this.response = response;
|
|
1194
|
+
this.requestId = requestId;
|
|
1195
|
+
this.context = context;
|
|
1196
|
+
this.metrics = metrics;
|
|
1197
|
+
this.trace = trace;
|
|
1198
|
+
this.startedAt = this.metrics?.streamFirstToken || this.trace?.streamEvent || this.trace?.streamError ? Date.now() : 0;
|
|
1268
1199
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1200
|
+
async cancel(reason) {
|
|
1201
|
+
this.closed = true;
|
|
1202
|
+
try {
|
|
1203
|
+
await this.response.body?.cancel(reason);
|
|
1204
|
+
} catch {
|
|
1205
|
+
}
|
|
1271
1206
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1207
|
+
async *[Symbol.asyncIterator]() {
|
|
1208
|
+
if (this.closed) {
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const body = this.response.body;
|
|
1212
|
+
if (!body) {
|
|
1213
|
+
throw new ConfigError("streaming response is missing a body");
|
|
1214
|
+
}
|
|
1215
|
+
const reader = body.getReader();
|
|
1216
|
+
const decoder = new TextDecoder();
|
|
1217
|
+
let buffer = "";
|
|
1218
|
+
try {
|
|
1219
|
+
while (true) {
|
|
1220
|
+
if (this.closed) {
|
|
1221
|
+
await reader.cancel();
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const { value, done } = await reader.read();
|
|
1225
|
+
if (done) {
|
|
1226
|
+
const { events: events2 } = consumeSSEBuffer(buffer, true);
|
|
1227
|
+
for (const evt of events2) {
|
|
1228
|
+
const parsed = mapChatEvent(evt, this.requestId);
|
|
1229
|
+
if (parsed) {
|
|
1230
|
+
this.handleStreamEvent(parsed);
|
|
1231
|
+
yield parsed;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1237
|
+
const { events, remainder } = consumeSSEBuffer(buffer);
|
|
1238
|
+
buffer = remainder;
|
|
1239
|
+
for (const evt of events) {
|
|
1240
|
+
const parsed = mapChatEvent(evt, this.requestId);
|
|
1241
|
+
if (parsed) {
|
|
1242
|
+
this.handleStreamEvent(parsed);
|
|
1243
|
+
yield parsed;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
this.recordFirstToken(err);
|
|
1249
|
+
this.trace?.streamError?.({ context: this.context, error: err });
|
|
1250
|
+
throw err;
|
|
1251
|
+
} finally {
|
|
1252
|
+
this.closed = true;
|
|
1253
|
+
reader.releaseLock();
|
|
1297
1254
|
}
|
|
1298
|
-
src.addEventListener(
|
|
1299
|
-
"abort",
|
|
1300
|
-
() => controller.abort(src.reason),
|
|
1301
|
-
{ once: true }
|
|
1302
|
-
);
|
|
1303
1255
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
if (k && v) {
|
|
1314
|
-
normalized[k] = v;
|
|
1256
|
+
handleStreamEvent(evt) {
|
|
1257
|
+
const context = this.enrichContext(evt);
|
|
1258
|
+
this.context = context;
|
|
1259
|
+
this.trace?.streamEvent?.({ context, event: evt });
|
|
1260
|
+
if (evt.type === "message_start" || evt.type === "message_delta" || evt.type === "message_stop" || evt.type === "tool_use_start" || evt.type === "tool_use_delta" || evt.type === "tool_use_stop") {
|
|
1261
|
+
this.recordFirstToken();
|
|
1262
|
+
}
|
|
1263
|
+
if (evt.type === "message_stop" && evt.usage && this.metrics?.usage) {
|
|
1264
|
+
this.metrics.usage({ usage: evt.usage, context });
|
|
1315
1265
|
}
|
|
1316
1266
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
lastError: typeof lastError === "string" ? lastError : lastError instanceof Error ? lastError.message : lastError ? String(lastError) : void 0
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
function classifyTransportErrorKind(err, connectTimedOut, requestTimedOut) {
|
|
1328
|
-
if (connectTimedOut) return "connect";
|
|
1329
|
-
if (requestTimedOut) return "timeout";
|
|
1330
|
-
if (err instanceof DOMException && err.name === "AbortError") {
|
|
1331
|
-
return requestTimedOut ? "timeout" : "request";
|
|
1267
|
+
enrichContext(evt) {
|
|
1268
|
+
return {
|
|
1269
|
+
...this.context,
|
|
1270
|
+
responseId: evt.responseId || this.context.responseId,
|
|
1271
|
+
requestId: evt.requestId || this.context.requestId,
|
|
1272
|
+
model: evt.model || this.context.model
|
|
1273
|
+
};
|
|
1332
1274
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
return new TransportError(message, { kind, retries, cause: err });
|
|
1339
|
-
}
|
|
1340
|
-
function recordHttpMetrics(metrics, trace, start, retries, info) {
|
|
1341
|
-
if (!metrics?.httpRequest && !trace?.requestFinish) return;
|
|
1342
|
-
const latencyMs = start ? Date.now() - start : 0;
|
|
1343
|
-
if (metrics?.httpRequest) {
|
|
1344
|
-
metrics.httpRequest({
|
|
1275
|
+
recordFirstToken(error) {
|
|
1276
|
+
if (!this.metrics?.streamFirstToken || this.firstTokenEmitted) return;
|
|
1277
|
+
this.firstTokenEmitted = true;
|
|
1278
|
+
const latencyMs = this.startedAt ? Date.now() - this.startedAt : 0;
|
|
1279
|
+
this.metrics.streamFirstToken({
|
|
1345
1280
|
latencyMs,
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
retries,
|
|
1349
|
-
context: info.context
|
|
1281
|
+
error: error ? String(error) : void 0,
|
|
1282
|
+
context: this.context
|
|
1350
1283
|
});
|
|
1351
1284
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
// src/tools.ts
|
|
1367
|
-
function zodToJsonSchema(schema, options = {}) {
|
|
1368
|
-
const result = convertZodType(schema);
|
|
1369
|
-
if (options.includeSchema) {
|
|
1370
|
-
const schemaVersion = options.target === "draft-04" ? "http://json-schema.org/draft-04/schema#" : options.target === "draft-2019-09" ? "https://json-schema.org/draft/2019-09/schema" : options.target === "draft-2020-12" ? "https://json-schema.org/draft/2020-12/schema" : "http://json-schema.org/draft-07/schema#";
|
|
1371
|
-
return { $schema: schemaVersion, ...result };
|
|
1285
|
+
};
|
|
1286
|
+
var StructuredJSONStream = class {
|
|
1287
|
+
constructor(response, requestId, context, metrics, trace) {
|
|
1288
|
+
this.closed = false;
|
|
1289
|
+
this.sawTerminal = false;
|
|
1290
|
+
if (!response.body) {
|
|
1291
|
+
throw new ConfigError("streaming response is missing a body");
|
|
1292
|
+
}
|
|
1293
|
+
this.response = response;
|
|
1294
|
+
this.requestId = requestId;
|
|
1295
|
+
this.context = context;
|
|
1296
|
+
this.metrics = metrics;
|
|
1297
|
+
this.trace = trace;
|
|
1372
1298
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
return
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1299
|
+
async cancel(reason) {
|
|
1300
|
+
this.closed = true;
|
|
1301
|
+
try {
|
|
1302
|
+
await this.response.body?.cancel(reason);
|
|
1303
|
+
} catch {
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
async *[Symbol.asyncIterator]() {
|
|
1307
|
+
if (this.closed) {
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const body = this.response.body;
|
|
1311
|
+
if (!body) {
|
|
1312
|
+
throw new ConfigError("streaming response is missing a body");
|
|
1313
|
+
}
|
|
1314
|
+
const reader = body.getReader();
|
|
1315
|
+
const decoder = new TextDecoder();
|
|
1316
|
+
let buffer = "";
|
|
1317
|
+
try {
|
|
1318
|
+
while (true) {
|
|
1319
|
+
if (this.closed) {
|
|
1320
|
+
await reader.cancel();
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
const { value, done } = await reader.read();
|
|
1324
|
+
if (done) {
|
|
1325
|
+
const { records: records2 } = consumeNDJSONBuffer(buffer, true);
|
|
1326
|
+
for (const line of records2) {
|
|
1327
|
+
const evt = this.parseRecord(line);
|
|
1328
|
+
if (evt) {
|
|
1329
|
+
this.traceStructuredEvent(evt, line);
|
|
1330
|
+
yield evt;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (!this.sawTerminal) {
|
|
1334
|
+
throw new TransportError(
|
|
1335
|
+
"structured stream ended without completion or error",
|
|
1336
|
+
{ kind: "request" }
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1342
|
+
const { records, remainder } = consumeNDJSONBuffer(buffer);
|
|
1343
|
+
buffer = remainder;
|
|
1344
|
+
for (const line of records) {
|
|
1345
|
+
const evt = this.parseRecord(line);
|
|
1346
|
+
if (evt) {
|
|
1347
|
+
this.traceStructuredEvent(evt, line);
|
|
1348
|
+
yield evt;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1403
1351
|
}
|
|
1404
|
-
|
|
1352
|
+
} catch (err) {
|
|
1353
|
+
this.trace?.streamError?.({ context: this.context, error: err });
|
|
1354
|
+
throw err;
|
|
1355
|
+
} finally {
|
|
1356
|
+
this.closed = true;
|
|
1357
|
+
reader.releaseLock();
|
|
1405
1358
|
}
|
|
1406
|
-
case "ZodNullable":
|
|
1407
|
-
return convertZodNullable(def);
|
|
1408
|
-
case "ZodDefault":
|
|
1409
|
-
return { ...convertZodType(def.innerType), default: def.defaultValue() };
|
|
1410
|
-
case "ZodEffects":
|
|
1411
|
-
return convertZodType(def.schema);
|
|
1412
|
-
case "ZodRecord":
|
|
1413
|
-
return convertZodRecord(def);
|
|
1414
|
-
case "ZodTuple":
|
|
1415
|
-
return convertZodTuple(def);
|
|
1416
|
-
case "ZodAny":
|
|
1417
|
-
case "ZodUnknown":
|
|
1418
|
-
return {};
|
|
1419
|
-
default:
|
|
1420
|
-
return {};
|
|
1421
1359
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
switch (check.kind) {
|
|
1429
|
-
case "min":
|
|
1430
|
-
result.minLength = check.value;
|
|
1431
|
-
break;
|
|
1432
|
-
case "max":
|
|
1433
|
-
result.maxLength = check.value;
|
|
1434
|
-
break;
|
|
1435
|
-
case "length":
|
|
1436
|
-
result.minLength = check.value;
|
|
1437
|
-
result.maxLength = check.value;
|
|
1438
|
-
break;
|
|
1439
|
-
case "email":
|
|
1440
|
-
result.format = "email";
|
|
1441
|
-
break;
|
|
1442
|
-
case "url":
|
|
1443
|
-
result.format = "uri";
|
|
1444
|
-
break;
|
|
1445
|
-
case "uuid":
|
|
1446
|
-
result.format = "uuid";
|
|
1447
|
-
break;
|
|
1448
|
-
case "datetime":
|
|
1449
|
-
result.format = "date-time";
|
|
1450
|
-
break;
|
|
1451
|
-
case "regex":
|
|
1452
|
-
result.pattern = check.value.source;
|
|
1453
|
-
break;
|
|
1360
|
+
async collect() {
|
|
1361
|
+
let last;
|
|
1362
|
+
for await (const evt of this) {
|
|
1363
|
+
last = evt;
|
|
1364
|
+
if (evt.type === "completion") {
|
|
1365
|
+
return evt.payload;
|
|
1454
1366
|
}
|
|
1455
1367
|
}
|
|
1368
|
+
throw new TransportError(
|
|
1369
|
+
"structured stream ended without completion or error",
|
|
1370
|
+
{ kind: "request" }
|
|
1371
|
+
);
|
|
1456
1372
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1373
|
+
parseRecord(line) {
|
|
1374
|
+
let parsed;
|
|
1375
|
+
try {
|
|
1376
|
+
parsed = JSON.parse(line);
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
throw new TransportError("invalid JSON in structured stream", {
|
|
1379
|
+
kind: "request",
|
|
1380
|
+
cause: err
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1384
|
+
throw new TransportError("structured stream record is not an object", {
|
|
1385
|
+
kind: "request"
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
const obj = parsed;
|
|
1389
|
+
const rawType = String(obj.type || "").trim().toLowerCase();
|
|
1390
|
+
if (!rawType) return null;
|
|
1391
|
+
if (rawType === "start") {
|
|
1392
|
+
return null;
|
|
1393
|
+
}
|
|
1394
|
+
if (rawType === "error") {
|
|
1395
|
+
this.sawTerminal = true;
|
|
1396
|
+
const status = typeof obj.status === "number" && obj.status > 0 ? obj.status : 500;
|
|
1397
|
+
const message = typeof obj.message === "string" && obj.message.trim() ? obj.message : "structured stream error";
|
|
1398
|
+
const code = typeof obj.code === "string" && obj.code.trim() ? obj.code : void 0;
|
|
1399
|
+
throw new APIError(message, {
|
|
1400
|
+
status,
|
|
1401
|
+
code,
|
|
1402
|
+
requestId: this.requestId
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
if (rawType !== "update" && rawType !== "completion") {
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
if (obj.payload === void 0 || obj.payload === null) {
|
|
1409
|
+
throw new TransportError(
|
|
1410
|
+
"structured stream record missing payload",
|
|
1411
|
+
{ kind: "request" }
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
if (rawType === "completion") {
|
|
1415
|
+
this.sawTerminal = true;
|
|
1416
|
+
}
|
|
1417
|
+
const event = {
|
|
1418
|
+
type: rawType,
|
|
1419
|
+
// biome-ignore lint/suspicious/noExplicitAny: payload is untyped json
|
|
1420
|
+
payload: obj.payload,
|
|
1421
|
+
requestId: this.requestId
|
|
1422
|
+
};
|
|
1423
|
+
return event;
|
|
1424
|
+
}
|
|
1425
|
+
traceStructuredEvent(evt, raw) {
|
|
1426
|
+
if (!this.trace?.streamEvent) return;
|
|
1427
|
+
const event = {
|
|
1428
|
+
type: "custom",
|
|
1429
|
+
event: "structured",
|
|
1430
|
+
data: { type: evt.type, payload: evt.payload },
|
|
1431
|
+
textDelta: void 0,
|
|
1432
|
+
toolCallDelta: void 0,
|
|
1433
|
+
toolCalls: void 0,
|
|
1434
|
+
responseId: void 0,
|
|
1435
|
+
model: void 0,
|
|
1436
|
+
stopReason: void 0,
|
|
1437
|
+
usage: void 0,
|
|
1438
|
+
requestId: this.requestId,
|
|
1439
|
+
raw
|
|
1440
|
+
};
|
|
1441
|
+
this.trace.streamEvent({ context: this.context, event });
|
|
1459
1442
|
}
|
|
1460
|
-
|
|
1443
|
+
};
|
|
1444
|
+
function consumeSSEBuffer(buffer, flush = false) {
|
|
1445
|
+
const events = [];
|
|
1446
|
+
let eventName = "";
|
|
1447
|
+
let dataLines = [];
|
|
1448
|
+
let remainder = "";
|
|
1449
|
+
const lines = buffer.split(/\r?\n/);
|
|
1450
|
+
const lastIndex = lines.length - 1;
|
|
1451
|
+
const limit = flush ? lines.length : Math.max(0, lastIndex);
|
|
1452
|
+
const pushEvent = () => {
|
|
1453
|
+
if (!eventName && dataLines.length === 0) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
events.push({
|
|
1457
|
+
event: eventName || "message",
|
|
1458
|
+
data: dataLines.join("\n")
|
|
1459
|
+
});
|
|
1460
|
+
eventName = "";
|
|
1461
|
+
dataLines = [];
|
|
1462
|
+
};
|
|
1463
|
+
for (let i = 0; i < limit; i++) {
|
|
1464
|
+
const line = lines[i];
|
|
1465
|
+
if (line === "") {
|
|
1466
|
+
pushEvent();
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
if (line.startsWith(":")) {
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
if (line.startsWith("event:")) {
|
|
1473
|
+
eventName = line.slice(6).trim();
|
|
1474
|
+
} else if (line.startsWith("data:")) {
|
|
1475
|
+
dataLines.push(line.slice(5).trimStart());
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (flush) {
|
|
1479
|
+
pushEvent();
|
|
1480
|
+
remainder = "";
|
|
1481
|
+
} else {
|
|
1482
|
+
remainder = lines[lastIndex] ?? "";
|
|
1483
|
+
}
|
|
1484
|
+
return { events, remainder };
|
|
1461
1485
|
}
|
|
1462
|
-
function
|
|
1463
|
-
const
|
|
1464
|
-
const
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
result.maximum = check.value;
|
|
1483
|
-
}
|
|
1484
|
-
break;
|
|
1485
|
-
case "multipleOf":
|
|
1486
|
-
result.multipleOf = check.value;
|
|
1487
|
-
break;
|
|
1488
|
-
}
|
|
1486
|
+
function consumeNDJSONBuffer(buffer, flush = false) {
|
|
1487
|
+
const lines = buffer.split(/\r?\n/);
|
|
1488
|
+
const records = [];
|
|
1489
|
+
const lastIndex = lines.length - 1;
|
|
1490
|
+
const limit = flush ? lines.length : Math.max(0, lastIndex);
|
|
1491
|
+
for (let i = 0; i < limit; i++) {
|
|
1492
|
+
const line = lines[i]?.trim();
|
|
1493
|
+
if (!line) continue;
|
|
1494
|
+
records.push(line);
|
|
1495
|
+
}
|
|
1496
|
+
const remainder = flush ? "" : lines[lastIndex] ?? "";
|
|
1497
|
+
return { records, remainder };
|
|
1498
|
+
}
|
|
1499
|
+
function mapChatEvent(raw, requestId) {
|
|
1500
|
+
let parsed = raw.data;
|
|
1501
|
+
if (raw.data) {
|
|
1502
|
+
try {
|
|
1503
|
+
parsed = JSON.parse(raw.data);
|
|
1504
|
+
} catch {
|
|
1505
|
+
parsed = raw.data;
|
|
1489
1506
|
}
|
|
1490
1507
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1508
|
+
const payload = typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
1509
|
+
const p = payload;
|
|
1510
|
+
const type = normalizeEventType(raw.event, p);
|
|
1511
|
+
const usage = normalizeUsage(p.usage);
|
|
1512
|
+
const responseId = p.response_id || p.id || p?.message?.id;
|
|
1513
|
+
const model = normalizeModelId(p.model || p?.message?.model);
|
|
1514
|
+
const stopReason = normalizeStopReason(p.stop_reason);
|
|
1515
|
+
const textDelta = extractTextDelta(p);
|
|
1516
|
+
const toolCallDelta = extractToolCallDelta(p, type);
|
|
1517
|
+
const toolCalls = extractToolCalls(p, type);
|
|
1518
|
+
return {
|
|
1519
|
+
type,
|
|
1520
|
+
event: raw.event || type,
|
|
1521
|
+
data: p,
|
|
1522
|
+
textDelta,
|
|
1523
|
+
toolCallDelta,
|
|
1524
|
+
toolCalls,
|
|
1525
|
+
responseId,
|
|
1526
|
+
model,
|
|
1527
|
+
stopReason,
|
|
1528
|
+
usage,
|
|
1529
|
+
requestId,
|
|
1530
|
+
raw: raw.data || ""
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
function normalizeEventType(eventName, payload) {
|
|
1534
|
+
const hint = String(
|
|
1535
|
+
payload?.type || payload?.event || eventName || ""
|
|
1536
|
+
).trim();
|
|
1537
|
+
switch (hint) {
|
|
1538
|
+
case "message_start":
|
|
1539
|
+
return "message_start";
|
|
1540
|
+
case "message_delta":
|
|
1541
|
+
return "message_delta";
|
|
1542
|
+
case "message_stop":
|
|
1543
|
+
return "message_stop";
|
|
1544
|
+
case "tool_use_start":
|
|
1545
|
+
return "tool_use_start";
|
|
1546
|
+
case "tool_use_delta":
|
|
1547
|
+
return "tool_use_delta";
|
|
1548
|
+
case "tool_use_stop":
|
|
1549
|
+
return "tool_use_stop";
|
|
1550
|
+
case "ping":
|
|
1551
|
+
return "ping";
|
|
1552
|
+
default:
|
|
1553
|
+
return "custom";
|
|
1493
1554
|
}
|
|
1494
|
-
return result;
|
|
1495
1555
|
}
|
|
1496
|
-
function
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
items: convertZodType(def.type)
|
|
1500
|
-
};
|
|
1501
|
-
if (def.minLength !== void 0 && def.minLength !== null) {
|
|
1502
|
-
result.minItems = def.minLength.value;
|
|
1556
|
+
function extractTextDelta(payload) {
|
|
1557
|
+
if (!payload || typeof payload !== "object") {
|
|
1558
|
+
return void 0;
|
|
1503
1559
|
}
|
|
1504
|
-
if (
|
|
1505
|
-
|
|
1560
|
+
if (typeof payload.text_delta === "string" && payload.text_delta !== "") {
|
|
1561
|
+
return payload.text_delta;
|
|
1506
1562
|
}
|
|
1507
|
-
if (
|
|
1508
|
-
|
|
1563
|
+
if (typeof payload.delta === "string") {
|
|
1564
|
+
return payload.delta;
|
|
1509
1565
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
const required = [];
|
|
1517
|
-
for (const [key, value] of Object.entries(shapeObj)) {
|
|
1518
|
-
properties[key] = convertZodType(value);
|
|
1519
|
-
const valueDef = value._def;
|
|
1520
|
-
const isOptional = valueDef.typeName === "ZodOptional" || valueDef.typeName === "ZodDefault" || valueDef.typeName === "ZodNullable" && valueDef.innerType?._def?.typeName === "ZodDefault";
|
|
1521
|
-
if (!isOptional) {
|
|
1522
|
-
required.push(key);
|
|
1566
|
+
if (payload.delta && typeof payload.delta === "object") {
|
|
1567
|
+
if (typeof payload.delta.text === "string") {
|
|
1568
|
+
return payload.delta.text;
|
|
1569
|
+
}
|
|
1570
|
+
if (typeof payload.delta.content === "string") {
|
|
1571
|
+
return payload.delta.content;
|
|
1523
1572
|
}
|
|
1524
1573
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
result.required = required;
|
|
1574
|
+
return void 0;
|
|
1575
|
+
}
|
|
1576
|
+
function extractToolCallDelta(payload, type) {
|
|
1577
|
+
if (!payload || typeof payload !== "object") {
|
|
1578
|
+
return void 0;
|
|
1531
1579
|
}
|
|
1532
|
-
if (
|
|
1533
|
-
|
|
1580
|
+
if (type !== "tool_use_start" && type !== "tool_use_delta") {
|
|
1581
|
+
return void 0;
|
|
1534
1582
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1583
|
+
if (payload.tool_call_delta) {
|
|
1584
|
+
const d = payload.tool_call_delta;
|
|
1585
|
+
return {
|
|
1586
|
+
index: d.index ?? 0,
|
|
1587
|
+
id: d.id,
|
|
1588
|
+
type: d.type,
|
|
1589
|
+
function: d.function ? {
|
|
1590
|
+
name: d.function.name,
|
|
1591
|
+
arguments: d.function.arguments
|
|
1592
|
+
} : void 0
|
|
1593
|
+
};
|
|
1538
1594
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1595
|
+
if (typeof payload.index === "number" || payload.id || payload.name) {
|
|
1596
|
+
return {
|
|
1597
|
+
index: payload.index ?? 0,
|
|
1598
|
+
id: payload.id,
|
|
1599
|
+
type: payload.tool_type,
|
|
1600
|
+
function: payload.name || payload.arguments ? {
|
|
1601
|
+
name: payload.name,
|
|
1602
|
+
arguments: payload.arguments
|
|
1603
|
+
} : void 0
|
|
1604
|
+
};
|
|
1548
1605
|
}
|
|
1549
|
-
return
|
|
1606
|
+
return void 0;
|
|
1550
1607
|
}
|
|
1551
|
-
function
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
(v) => typeof v === "string" || typeof v === "number"
|
|
1555
|
-
);
|
|
1556
|
-
const result = { enum: values };
|
|
1557
|
-
if (def.description) {
|
|
1558
|
-
result.description = def.description;
|
|
1608
|
+
function extractToolCalls(payload, type) {
|
|
1609
|
+
if (!payload || typeof payload !== "object") {
|
|
1610
|
+
return void 0;
|
|
1559
1611
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
function convertZodUnion(def) {
|
|
1563
|
-
const options = def.options;
|
|
1564
|
-
const result = {
|
|
1565
|
-
anyOf: options.map(convertZodType)
|
|
1566
|
-
};
|
|
1567
|
-
if (def.description) {
|
|
1568
|
-
result.description = def.description;
|
|
1612
|
+
if (type !== "tool_use_stop" && type !== "message_stop") {
|
|
1613
|
+
return void 0;
|
|
1569
1614
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
function convertZodNullable(def) {
|
|
1573
|
-
const inner = convertZodType(def.innerType);
|
|
1574
|
-
return {
|
|
1575
|
-
anyOf: [inner, { type: "null" }]
|
|
1576
|
-
};
|
|
1577
|
-
}
|
|
1578
|
-
function convertZodRecord(def) {
|
|
1579
|
-
const result = {
|
|
1580
|
-
type: "object",
|
|
1581
|
-
additionalProperties: convertZodType(def.valueType)
|
|
1582
|
-
};
|
|
1583
|
-
if (def.description) {
|
|
1584
|
-
result.description = def.description;
|
|
1615
|
+
if (payload.tool_calls?.length) {
|
|
1616
|
+
return normalizeToolCalls(payload.tool_calls);
|
|
1585
1617
|
}
|
|
1586
|
-
|
|
1618
|
+
if (payload.tool_call) {
|
|
1619
|
+
return normalizeToolCalls([payload.tool_call]);
|
|
1620
|
+
}
|
|
1621
|
+
return void 0;
|
|
1587
1622
|
}
|
|
1588
|
-
function
|
|
1589
|
-
const
|
|
1590
|
-
const
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1623
|
+
function normalizeChatResponse(payload, requestId) {
|
|
1624
|
+
const p = payload;
|
|
1625
|
+
const response = {
|
|
1626
|
+
id: p?.id,
|
|
1627
|
+
content: Array.isArray(p?.content) ? p.content : p?.content ? [String(p.content)] : [],
|
|
1628
|
+
stopReason: normalizeStopReason(p?.stop_reason),
|
|
1629
|
+
model: normalizeModelId(p?.model),
|
|
1630
|
+
usage: normalizeUsage(p?.usage),
|
|
1631
|
+
requestId
|
|
1595
1632
|
};
|
|
1596
|
-
if (
|
|
1597
|
-
|
|
1633
|
+
if (p?.tool_calls?.length) {
|
|
1634
|
+
response.toolCalls = normalizeToolCalls(p.tool_calls);
|
|
1598
1635
|
}
|
|
1599
|
-
return
|
|
1636
|
+
return response;
|
|
1600
1637
|
}
|
|
1601
|
-
function
|
|
1602
|
-
|
|
1603
|
-
|
|
1638
|
+
function normalizeToolCalls(toolCalls) {
|
|
1639
|
+
return toolCalls.map(
|
|
1640
|
+
(tc) => createToolCall(
|
|
1641
|
+
tc.id,
|
|
1642
|
+
tc.function?.name ?? "",
|
|
1643
|
+
tc.function?.arguments ?? "",
|
|
1644
|
+
tc.type || ToolTypes.Function
|
|
1645
|
+
)
|
|
1646
|
+
);
|
|
1604
1647
|
}
|
|
1605
|
-
function
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
fn.parameters = parameters;
|
|
1648
|
+
function normalizeUsage(payload) {
|
|
1649
|
+
if (!payload) {
|
|
1650
|
+
return createUsage(0, 0, 0);
|
|
1609
1651
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1652
|
+
const inputTokens = Number(payload.input_tokens ?? 0);
|
|
1653
|
+
const outputTokens = Number(payload.output_tokens ?? 0);
|
|
1654
|
+
const totalTokens = Number(payload.total_tokens ?? 0);
|
|
1655
|
+
return createUsage(inputTokens, outputTokens, totalTokens || void 0);
|
|
1614
1656
|
}
|
|
1615
|
-
function
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
allowedDomains: options.allowedDomains,
|
|
1620
|
-
excludedDomains: options.excludedDomains,
|
|
1621
|
-
maxUses: options.maxUses
|
|
1622
|
-
} : void 0
|
|
1657
|
+
function buildProxyBody(params, metadata) {
|
|
1658
|
+
const modelValue = params.model ? modelToString(params.model).trim() : "";
|
|
1659
|
+
const body = {
|
|
1660
|
+
messages: normalizeMessages(params.messages)
|
|
1623
1661
|
};
|
|
1662
|
+
if (modelValue) {
|
|
1663
|
+
body.model = modelValue;
|
|
1664
|
+
}
|
|
1665
|
+
if (typeof params.maxTokens === "number") body.max_tokens = params.maxTokens;
|
|
1666
|
+
if (typeof params.temperature === "number")
|
|
1667
|
+
body.temperature = params.temperature;
|
|
1668
|
+
if (metadata && Object.keys(metadata).length > 0) body.metadata = metadata;
|
|
1669
|
+
if (params.stop?.length) body.stop = params.stop;
|
|
1670
|
+
if (params.stopSequences?.length) body.stop_sequences = params.stopSequences;
|
|
1671
|
+
if (params.tools?.length) body.tools = normalizeTools(params.tools);
|
|
1672
|
+
if (params.toolChoice) body.tool_choice = normalizeToolChoice(params.toolChoice);
|
|
1673
|
+
if (params.responseFormat) body.response_format = params.responseFormat;
|
|
1674
|
+
return body;
|
|
1624
1675
|
}
|
|
1625
|
-
function
|
|
1626
|
-
return
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1676
|
+
function normalizeMessages(messages) {
|
|
1677
|
+
return messages.map((msg) => {
|
|
1678
|
+
const normalized = {
|
|
1679
|
+
role: msg.role || "user",
|
|
1680
|
+
content: msg.content
|
|
1681
|
+
};
|
|
1682
|
+
if (msg.toolCalls?.length) {
|
|
1683
|
+
normalized.tool_calls = msg.toolCalls.map((tc) => ({
|
|
1684
|
+
id: tc.id,
|
|
1685
|
+
type: tc.type,
|
|
1686
|
+
function: tc.function ? createFunctionCall(tc.function.name, tc.function.arguments) : void 0
|
|
1687
|
+
}));
|
|
1688
|
+
}
|
|
1689
|
+
if (msg.toolCallId) {
|
|
1690
|
+
normalized.tool_call_id = msg.toolCallId;
|
|
1691
|
+
}
|
|
1692
|
+
return normalized;
|
|
1693
|
+
});
|
|
1630
1694
|
}
|
|
1631
|
-
function
|
|
1632
|
-
return
|
|
1695
|
+
function normalizeTools(tools) {
|
|
1696
|
+
return tools.map((tool) => {
|
|
1697
|
+
const normalized = { type: tool.type };
|
|
1698
|
+
if (tool.function) {
|
|
1699
|
+
normalized.function = {
|
|
1700
|
+
name: tool.function.name,
|
|
1701
|
+
description: tool.function.description,
|
|
1702
|
+
parameters: tool.function.parameters
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
if (tool.web) {
|
|
1706
|
+
const web = {
|
|
1707
|
+
allowed_domains: tool.web.allowedDomains,
|
|
1708
|
+
excluded_domains: tool.web.excludedDomains,
|
|
1709
|
+
max_uses: tool.web.maxUses
|
|
1710
|
+
};
|
|
1711
|
+
if (tool.web.mode) {
|
|
1712
|
+
web.mode = tool.web.mode;
|
|
1713
|
+
}
|
|
1714
|
+
normalized.web = web;
|
|
1715
|
+
}
|
|
1716
|
+
if (tool.xSearch) {
|
|
1717
|
+
normalized.x_search = {
|
|
1718
|
+
allowed_handles: tool.xSearch.allowedHandles,
|
|
1719
|
+
excluded_handles: tool.xSearch.excludedHandles,
|
|
1720
|
+
from_date: tool.xSearch.fromDate,
|
|
1721
|
+
to_date: tool.xSearch.toDate
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
if (tool.codeExecution) {
|
|
1725
|
+
normalized.code_execution = {
|
|
1726
|
+
language: tool.codeExecution.language,
|
|
1727
|
+
timeout_ms: tool.codeExecution.timeoutMs
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
return normalized;
|
|
1731
|
+
});
|
|
1633
1732
|
}
|
|
1634
|
-
function
|
|
1635
|
-
return
|
|
1733
|
+
function normalizeToolChoice(tc) {
|
|
1734
|
+
return { type: tc.type };
|
|
1636
1735
|
}
|
|
1637
|
-
function
|
|
1638
|
-
return
|
|
1736
|
+
function requestIdFromHeaders(headers) {
|
|
1737
|
+
return headers.get(REQUEST_ID_HEADER) || headers.get("X-Request-Id") || void 0;
|
|
1639
1738
|
}
|
|
1640
|
-
function
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1739
|
+
function mergeMetadata(...sources) {
|
|
1740
|
+
const merged = {};
|
|
1741
|
+
for (const src of sources) {
|
|
1742
|
+
if (!src) continue;
|
|
1743
|
+
for (const [key, value] of Object.entries(src)) {
|
|
1744
|
+
const k = key?.trim();
|
|
1745
|
+
const v = value?.trim();
|
|
1746
|
+
if (!k || !v) continue;
|
|
1747
|
+
merged[k] = v;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
return Object.keys(merged).length ? merged : void 0;
|
|
1647
1751
|
}
|
|
1648
|
-
function
|
|
1649
|
-
return
|
|
1752
|
+
function hasUserMessage(messages) {
|
|
1753
|
+
return messages.some(
|
|
1754
|
+
(msg) => msg.role?.toLowerCase?.() === "user" && !!msg.content
|
|
1755
|
+
);
|
|
1650
1756
|
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
};
|
|
1757
|
+
|
|
1758
|
+
// src/customers.ts
|
|
1759
|
+
var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1760
|
+
function isValidEmail(email) {
|
|
1761
|
+
return EMAIL_REGEX.test(email);
|
|
1657
1762
|
}
|
|
1658
|
-
var
|
|
1659
|
-
constructor() {
|
|
1660
|
-
this.
|
|
1763
|
+
var CustomersClient = class {
|
|
1764
|
+
constructor(http, cfg) {
|
|
1765
|
+
this.http = http;
|
|
1766
|
+
this.apiKey = cfg.apiKey;
|
|
1767
|
+
}
|
|
1768
|
+
ensureSecretKey() {
|
|
1769
|
+
if (!this.apiKey || !this.apiKey.startsWith("mr_sk_")) {
|
|
1770
|
+
throw new ConfigError(
|
|
1771
|
+
"Secret key (mr_sk_*) required for customer operations"
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1661
1774
|
}
|
|
1662
1775
|
/**
|
|
1663
|
-
*
|
|
1664
|
-
* Returns true if this started a new tool call.
|
|
1776
|
+
* List all customers in the project.
|
|
1665
1777
|
*/
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1778
|
+
async list() {
|
|
1779
|
+
this.ensureSecretKey();
|
|
1780
|
+
const response = await this.http.json("/customers", {
|
|
1781
|
+
method: "GET",
|
|
1782
|
+
apiKey: this.apiKey
|
|
1783
|
+
});
|
|
1784
|
+
return response.customers;
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Create a new customer in the project.
|
|
1788
|
+
*/
|
|
1789
|
+
async create(request) {
|
|
1790
|
+
this.ensureSecretKey();
|
|
1791
|
+
if (!request.tier_id?.trim()) {
|
|
1792
|
+
throw new ConfigError("tier_id is required");
|
|
1678
1793
|
}
|
|
1679
|
-
if (
|
|
1680
|
-
|
|
1681
|
-
existing.function = existing.function ?? { name: "", arguments: "" };
|
|
1682
|
-
existing.function.name = delta.function.name;
|
|
1683
|
-
}
|
|
1684
|
-
if (delta.function.arguments) {
|
|
1685
|
-
existing.function = existing.function ?? { name: "", arguments: "" };
|
|
1686
|
-
existing.function.arguments += delta.function.arguments;
|
|
1687
|
-
}
|
|
1794
|
+
if (!request.external_id?.trim()) {
|
|
1795
|
+
throw new ConfigError("external_id is required");
|
|
1688
1796
|
}
|
|
1689
|
-
|
|
1797
|
+
if (!request.email?.trim()) {
|
|
1798
|
+
throw new ConfigError("email is required");
|
|
1799
|
+
}
|
|
1800
|
+
if (!isValidEmail(request.email)) {
|
|
1801
|
+
throw new ConfigError("invalid email format");
|
|
1802
|
+
}
|
|
1803
|
+
const response = await this.http.json("/customers", {
|
|
1804
|
+
method: "POST",
|
|
1805
|
+
body: request,
|
|
1806
|
+
apiKey: this.apiKey
|
|
1807
|
+
});
|
|
1808
|
+
return response.customer;
|
|
1690
1809
|
}
|
|
1691
1810
|
/**
|
|
1692
|
-
*
|
|
1811
|
+
* Get a customer by ID.
|
|
1693
1812
|
*/
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1813
|
+
async get(customerId) {
|
|
1814
|
+
this.ensureSecretKey();
|
|
1815
|
+
if (!customerId?.trim()) {
|
|
1816
|
+
throw new ConfigError("customerId is required");
|
|
1697
1817
|
}
|
|
1698
|
-
const
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
result.push(call);
|
|
1818
|
+
const response = await this.http.json(
|
|
1819
|
+
`/customers/${customerId}`,
|
|
1820
|
+
{
|
|
1821
|
+
method: "GET",
|
|
1822
|
+
apiKey: this.apiKey
|
|
1704
1823
|
}
|
|
1705
|
-
|
|
1706
|
-
return
|
|
1824
|
+
);
|
|
1825
|
+
return response.customer;
|
|
1707
1826
|
}
|
|
1708
1827
|
/**
|
|
1709
|
-
*
|
|
1828
|
+
* Upsert a customer by external_id.
|
|
1829
|
+
* If a customer with the given external_id exists, it is updated.
|
|
1830
|
+
* Otherwise, a new customer is created.
|
|
1710
1831
|
*/
|
|
1711
|
-
|
|
1712
|
-
|
|
1832
|
+
async upsert(request) {
|
|
1833
|
+
this.ensureSecretKey();
|
|
1834
|
+
if (!request.tier_id?.trim()) {
|
|
1835
|
+
throw new ConfigError("tier_id is required");
|
|
1836
|
+
}
|
|
1837
|
+
if (!request.external_id?.trim()) {
|
|
1838
|
+
throw new ConfigError("external_id is required");
|
|
1839
|
+
}
|
|
1840
|
+
if (!request.email?.trim()) {
|
|
1841
|
+
throw new ConfigError("email is required");
|
|
1842
|
+
}
|
|
1843
|
+
if (!isValidEmail(request.email)) {
|
|
1844
|
+
throw new ConfigError("invalid email format");
|
|
1845
|
+
}
|
|
1846
|
+
const response = await this.http.json("/customers", {
|
|
1847
|
+
method: "PUT",
|
|
1848
|
+
body: request,
|
|
1849
|
+
apiKey: this.apiKey
|
|
1850
|
+
});
|
|
1851
|
+
return response.customer;
|
|
1713
1852
|
}
|
|
1714
1853
|
/**
|
|
1715
|
-
*
|
|
1854
|
+
* Claim a customer by email, setting their external_id.
|
|
1855
|
+
* Used when a customer subscribes via Stripe Checkout (email only) and later
|
|
1856
|
+
* authenticates to the app, needing to link their identity.
|
|
1857
|
+
*
|
|
1858
|
+
* @throws {APIError} with status 404 if customer not found by email
|
|
1859
|
+
* @throws {APIError} with status 409 if customer already claimed or external_id in use
|
|
1716
1860
|
*/
|
|
1717
|
-
|
|
1718
|
-
this.
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
var ToolArgsError = class extends Error {
|
|
1722
|
-
constructor(message, toolCallId, toolName, rawArguments) {
|
|
1723
|
-
super(message);
|
|
1724
|
-
this.name = "ToolArgsError";
|
|
1725
|
-
this.toolCallId = toolCallId;
|
|
1726
|
-
this.toolName = toolName;
|
|
1727
|
-
this.rawArguments = rawArguments;
|
|
1728
|
-
}
|
|
1729
|
-
};
|
|
1730
|
-
function parseToolArgs(call, schema) {
|
|
1731
|
-
const toolName = call.function?.name ?? "unknown";
|
|
1732
|
-
const rawArgs = call.function?.arguments ?? "";
|
|
1733
|
-
let parsed;
|
|
1734
|
-
try {
|
|
1735
|
-
parsed = rawArgs ? JSON.parse(rawArgs) : {};
|
|
1736
|
-
} catch (err) {
|
|
1737
|
-
const message = err instanceof Error ? err.message : "Invalid JSON in arguments";
|
|
1738
|
-
throw new ToolArgsError(
|
|
1739
|
-
`Failed to parse arguments for tool '${toolName}': ${message}`,
|
|
1740
|
-
call.id,
|
|
1741
|
-
toolName,
|
|
1742
|
-
rawArgs
|
|
1743
|
-
);
|
|
1744
|
-
}
|
|
1745
|
-
try {
|
|
1746
|
-
return schema.parse(parsed);
|
|
1747
|
-
} catch (err) {
|
|
1748
|
-
let message;
|
|
1749
|
-
if (err instanceof Error) {
|
|
1750
|
-
const zodErr = err;
|
|
1751
|
-
if (zodErr.errors && Array.isArray(zodErr.errors)) {
|
|
1752
|
-
const issues = zodErr.errors.map((e) => {
|
|
1753
|
-
const path = e.path.length > 0 ? `${e.path.join(".")}: ` : "";
|
|
1754
|
-
return `${path}${e.message}`;
|
|
1755
|
-
}).join("; ");
|
|
1756
|
-
message = issues;
|
|
1757
|
-
} else {
|
|
1758
|
-
message = err.message;
|
|
1759
|
-
}
|
|
1760
|
-
} else {
|
|
1761
|
-
message = String(err);
|
|
1861
|
+
async claim(request) {
|
|
1862
|
+
this.ensureSecretKey();
|
|
1863
|
+
if (!request.email?.trim()) {
|
|
1864
|
+
throw new ConfigError("email is required");
|
|
1762
1865
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
call.id,
|
|
1766
|
-
toolName,
|
|
1767
|
-
rawArgs
|
|
1768
|
-
);
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
function tryParseToolArgs(call, schema) {
|
|
1772
|
-
try {
|
|
1773
|
-
const data = parseToolArgs(call, schema);
|
|
1774
|
-
return { success: true, data };
|
|
1775
|
-
} catch (err) {
|
|
1776
|
-
if (err instanceof ToolArgsError) {
|
|
1777
|
-
return { success: false, error: err };
|
|
1866
|
+
if (!isValidEmail(request.email)) {
|
|
1867
|
+
throw new ConfigError("invalid email format");
|
|
1778
1868
|
}
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
)
|
|
1789
|
-
};
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
function parseToolArgsRaw(call) {
|
|
1793
|
-
const toolName = call.function?.name ?? "unknown";
|
|
1794
|
-
const rawArgs = call.function?.arguments ?? "";
|
|
1795
|
-
try {
|
|
1796
|
-
return rawArgs ? JSON.parse(rawArgs) : {};
|
|
1797
|
-
} catch (err) {
|
|
1798
|
-
const message = err instanceof Error ? err.message : "Invalid JSON in arguments";
|
|
1799
|
-
throw new ToolArgsError(
|
|
1800
|
-
`Failed to parse arguments for tool '${toolName}': ${message}`,
|
|
1801
|
-
call.id,
|
|
1802
|
-
toolName,
|
|
1803
|
-
rawArgs
|
|
1804
|
-
);
|
|
1869
|
+
if (!request.external_id?.trim()) {
|
|
1870
|
+
throw new ConfigError("external_id is required");
|
|
1871
|
+
}
|
|
1872
|
+
const response = await this.http.json("/customers/claim", {
|
|
1873
|
+
method: "POST",
|
|
1874
|
+
body: request,
|
|
1875
|
+
apiKey: this.apiKey
|
|
1876
|
+
});
|
|
1877
|
+
return response.customer;
|
|
1805
1878
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1879
|
+
/**
|
|
1880
|
+
* Delete a customer by ID.
|
|
1881
|
+
*/
|
|
1882
|
+
async delete(customerId) {
|
|
1883
|
+
this.ensureSecretKey();
|
|
1884
|
+
if (!customerId?.trim()) {
|
|
1885
|
+
throw new ConfigError("customerId is required");
|
|
1886
|
+
}
|
|
1887
|
+
await this.http.request(`/customers/${customerId}`, {
|
|
1888
|
+
method: "DELETE",
|
|
1889
|
+
apiKey: this.apiKey
|
|
1890
|
+
});
|
|
1810
1891
|
}
|
|
1811
1892
|
/**
|
|
1812
|
-
*
|
|
1813
|
-
* @param name - The tool name (must match the function name in the tool definition)
|
|
1814
|
-
* @param handler - Function to execute when this tool is called
|
|
1815
|
-
* @returns this for chaining
|
|
1893
|
+
* Create a Stripe checkout session for a customer.
|
|
1816
1894
|
*/
|
|
1817
|
-
|
|
1818
|
-
this.
|
|
1819
|
-
|
|
1895
|
+
async createCheckoutSession(customerId, request) {
|
|
1896
|
+
this.ensureSecretKey();
|
|
1897
|
+
if (!customerId?.trim()) {
|
|
1898
|
+
throw new ConfigError("customerId is required");
|
|
1899
|
+
}
|
|
1900
|
+
if (!request.success_url?.trim() || !request.cancel_url?.trim()) {
|
|
1901
|
+
throw new ConfigError("success_url and cancel_url are required");
|
|
1902
|
+
}
|
|
1903
|
+
return await this.http.json(
|
|
1904
|
+
`/customers/${customerId}/checkout`,
|
|
1905
|
+
{
|
|
1906
|
+
method: "POST",
|
|
1907
|
+
body: request,
|
|
1908
|
+
apiKey: this.apiKey
|
|
1909
|
+
}
|
|
1910
|
+
);
|
|
1820
1911
|
}
|
|
1821
1912
|
/**
|
|
1822
|
-
*
|
|
1823
|
-
* @param name - The tool name to unregister
|
|
1824
|
-
* @returns true if the handler was removed, false if it didn't exist
|
|
1913
|
+
* Get the subscription status for a customer.
|
|
1825
1914
|
*/
|
|
1826
|
-
|
|
1827
|
-
|
|
1915
|
+
async getSubscription(customerId) {
|
|
1916
|
+
this.ensureSecretKey();
|
|
1917
|
+
if (!customerId?.trim()) {
|
|
1918
|
+
throw new ConfigError("customerId is required");
|
|
1919
|
+
}
|
|
1920
|
+
return await this.http.json(
|
|
1921
|
+
`/customers/${customerId}/subscription`,
|
|
1922
|
+
{
|
|
1923
|
+
method: "GET",
|
|
1924
|
+
apiKey: this.apiKey
|
|
1925
|
+
}
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1930
|
+
// src/tiers.ts
|
|
1931
|
+
var TiersClient = class {
|
|
1932
|
+
constructor(http, cfg) {
|
|
1933
|
+
this.http = http;
|
|
1934
|
+
this.apiKey = cfg.apiKey;
|
|
1935
|
+
}
|
|
1936
|
+
ensureApiKey() {
|
|
1937
|
+
if (!this.apiKey || !this.apiKey.startsWith("mr_pk_") && !this.apiKey.startsWith("mr_sk_")) {
|
|
1938
|
+
throw new ConfigError(
|
|
1939
|
+
"API key (mr_pk_* or mr_sk_*) required for tier operations"
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1828
1942
|
}
|
|
1829
1943
|
/**
|
|
1830
|
-
*
|
|
1944
|
+
* List all tiers in the project.
|
|
1831
1945
|
*/
|
|
1832
|
-
|
|
1833
|
-
|
|
1946
|
+
async list() {
|
|
1947
|
+
this.ensureApiKey();
|
|
1948
|
+
const response = await this.http.json("/tiers", {
|
|
1949
|
+
method: "GET",
|
|
1950
|
+
apiKey: this.apiKey
|
|
1951
|
+
});
|
|
1952
|
+
return response.tiers;
|
|
1834
1953
|
}
|
|
1835
1954
|
/**
|
|
1836
|
-
*
|
|
1955
|
+
* Get a tier by ID.
|
|
1837
1956
|
*/
|
|
1838
|
-
|
|
1839
|
-
|
|
1957
|
+
async get(tierId) {
|
|
1958
|
+
this.ensureApiKey();
|
|
1959
|
+
if (!tierId?.trim()) {
|
|
1960
|
+
throw new ConfigError("tierId is required");
|
|
1961
|
+
}
|
|
1962
|
+
const response = await this.http.json(`/tiers/${tierId}`, {
|
|
1963
|
+
method: "GET",
|
|
1964
|
+
apiKey: this.apiKey
|
|
1965
|
+
});
|
|
1966
|
+
return response.tier;
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
|
|
1970
|
+
// src/http.ts
|
|
1971
|
+
var HTTPClient = class {
|
|
1972
|
+
constructor(cfg) {
|
|
1973
|
+
const resolvedBase = normalizeBaseUrl(cfg.baseUrl || DEFAULT_BASE_URL);
|
|
1974
|
+
if (!isValidHttpUrl(resolvedBase)) {
|
|
1975
|
+
throw new ConfigError(
|
|
1976
|
+
"baseUrl must start with http:// or https://"
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
this.baseUrl = resolvedBase;
|
|
1980
|
+
this.apiKey = cfg.apiKey?.trim();
|
|
1981
|
+
this.accessToken = cfg.accessToken?.trim();
|
|
1982
|
+
this.fetchImpl = cfg.fetchImpl;
|
|
1983
|
+
this.clientHeader = cfg.clientHeader?.trim() || DEFAULT_CLIENT_HEADER;
|
|
1984
|
+
this.defaultConnectTimeoutMs = cfg.connectTimeoutMs === void 0 ? DEFAULT_CONNECT_TIMEOUT_MS : Math.max(0, cfg.connectTimeoutMs);
|
|
1985
|
+
this.defaultTimeoutMs = cfg.timeoutMs === void 0 ? DEFAULT_REQUEST_TIMEOUT_MS : Math.max(0, cfg.timeoutMs);
|
|
1986
|
+
this.retry = normalizeRetryConfig(cfg.retry);
|
|
1987
|
+
this.defaultHeaders = normalizeHeaders(cfg.defaultHeaders);
|
|
1988
|
+
this.metrics = cfg.metrics;
|
|
1989
|
+
this.trace = cfg.trace;
|
|
1990
|
+
}
|
|
1991
|
+
async request(path, options = {}) {
|
|
1992
|
+
const fetchFn = this.fetchImpl ?? globalThis.fetch;
|
|
1993
|
+
if (!fetchFn) {
|
|
1994
|
+
throw new ConfigError(
|
|
1995
|
+
"fetch is not available; provide a fetch implementation"
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
const method = options.method || "GET";
|
|
1999
|
+
const url = buildUrl(this.baseUrl, path);
|
|
2000
|
+
const metrics = mergeMetrics(this.metrics, options.metrics);
|
|
2001
|
+
const trace = mergeTrace(this.trace, options.trace);
|
|
2002
|
+
const context = {
|
|
2003
|
+
method,
|
|
2004
|
+
path,
|
|
2005
|
+
...options.context || {}
|
|
2006
|
+
};
|
|
2007
|
+
trace?.requestStart?.(context);
|
|
2008
|
+
const start = metrics?.httpRequest || trace?.requestFinish ? Date.now() : 0;
|
|
2009
|
+
const headers = new Headers({
|
|
2010
|
+
...this.defaultHeaders,
|
|
2011
|
+
...options.headers || {}
|
|
2012
|
+
});
|
|
2013
|
+
const accepts = options.accept || (options.raw ? void 0 : "application/json");
|
|
2014
|
+
if (accepts && !headers.has("Accept")) {
|
|
2015
|
+
headers.set("Accept", accepts);
|
|
2016
|
+
}
|
|
2017
|
+
const body = options.body;
|
|
2018
|
+
const shouldEncodeJSON = body !== void 0 && body !== null && typeof body === "object" && !(body instanceof FormData) && !(body instanceof Blob);
|
|
2019
|
+
const payload = shouldEncodeJSON ? JSON.stringify(body) : body;
|
|
2020
|
+
if (shouldEncodeJSON && !headers.has("Content-Type")) {
|
|
2021
|
+
headers.set("Content-Type", "application/json");
|
|
2022
|
+
}
|
|
2023
|
+
const accessToken = options.accessToken ?? this.accessToken;
|
|
2024
|
+
if (accessToken) {
|
|
2025
|
+
const bearer = accessToken.toLowerCase().startsWith("bearer ") ? accessToken : `Bearer ${accessToken}`;
|
|
2026
|
+
headers.set("Authorization", bearer);
|
|
2027
|
+
}
|
|
2028
|
+
const apiKey = options.apiKey ?? this.apiKey;
|
|
2029
|
+
if (apiKey) {
|
|
2030
|
+
headers.set("X-ModelRelay-Api-Key", apiKey);
|
|
2031
|
+
}
|
|
2032
|
+
if (this.clientHeader && !headers.has("X-ModelRelay-Client")) {
|
|
2033
|
+
headers.set("X-ModelRelay-Client", this.clientHeader);
|
|
2034
|
+
}
|
|
2035
|
+
const timeoutMs = options.useDefaultTimeout === false ? options.timeoutMs : options.timeoutMs ?? this.defaultTimeoutMs;
|
|
2036
|
+
const connectTimeoutMs = options.useDefaultConnectTimeout === false ? options.connectTimeoutMs : options.connectTimeoutMs ?? this.defaultConnectTimeoutMs;
|
|
2037
|
+
const retryCfg = normalizeRetryConfig(
|
|
2038
|
+
options.retry === void 0 ? this.retry : options.retry
|
|
2039
|
+
);
|
|
2040
|
+
const attempts = retryCfg ? Math.max(1, retryCfg.maxAttempts) : 1;
|
|
2041
|
+
let lastError;
|
|
2042
|
+
let lastStatus;
|
|
2043
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
2044
|
+
let connectTimedOut = false;
|
|
2045
|
+
let requestTimedOut = false;
|
|
2046
|
+
const connectController = connectTimeoutMs && connectTimeoutMs > 0 ? new AbortController() : void 0;
|
|
2047
|
+
const requestController = timeoutMs && timeoutMs > 0 ? new AbortController() : void 0;
|
|
2048
|
+
const signal = mergeSignals(
|
|
2049
|
+
options.signal,
|
|
2050
|
+
connectController?.signal,
|
|
2051
|
+
requestController?.signal
|
|
2052
|
+
);
|
|
2053
|
+
const connectTimer = connectController && setTimeout(() => {
|
|
2054
|
+
connectTimedOut = true;
|
|
2055
|
+
connectController.abort(
|
|
2056
|
+
new DOMException("connect timeout", "AbortError")
|
|
2057
|
+
);
|
|
2058
|
+
}, connectTimeoutMs);
|
|
2059
|
+
const requestTimer = requestController && setTimeout(() => {
|
|
2060
|
+
requestTimedOut = true;
|
|
2061
|
+
requestController.abort(
|
|
2062
|
+
new DOMException("timeout", "AbortError")
|
|
2063
|
+
);
|
|
2064
|
+
}, timeoutMs);
|
|
2065
|
+
try {
|
|
2066
|
+
const response = await fetchFn(url, {
|
|
2067
|
+
method,
|
|
2068
|
+
headers,
|
|
2069
|
+
body: payload,
|
|
2070
|
+
signal
|
|
2071
|
+
});
|
|
2072
|
+
if (connectTimer) {
|
|
2073
|
+
clearTimeout(connectTimer);
|
|
2074
|
+
}
|
|
2075
|
+
if (!response.ok) {
|
|
2076
|
+
const shouldRetry = retryCfg && shouldRetryStatus(
|
|
2077
|
+
response.status,
|
|
2078
|
+
method,
|
|
2079
|
+
retryCfg.retryPost
|
|
2080
|
+
) && attempt < attempts;
|
|
2081
|
+
if (shouldRetry) {
|
|
2082
|
+
lastStatus = response.status;
|
|
2083
|
+
await backoff(attempt, retryCfg);
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
2086
|
+
const retries = buildRetryMetadata(attempt, response.status, lastError);
|
|
2087
|
+
const finishedCtx2 = withRequestId(context, response.headers);
|
|
2088
|
+
recordHttpMetrics(metrics, trace, start, retries, {
|
|
2089
|
+
status: response.status,
|
|
2090
|
+
context: finishedCtx2
|
|
2091
|
+
});
|
|
2092
|
+
throw options.raw ? await parseErrorResponse(response, retries) : await parseErrorResponse(response, retries);
|
|
2093
|
+
}
|
|
2094
|
+
const finishedCtx = withRequestId(context, response.headers);
|
|
2095
|
+
recordHttpMetrics(metrics, trace, start, void 0, {
|
|
2096
|
+
status: response.status,
|
|
2097
|
+
context: finishedCtx
|
|
2098
|
+
});
|
|
2099
|
+
return response;
|
|
2100
|
+
} catch (err) {
|
|
2101
|
+
if (options.signal?.aborted) {
|
|
2102
|
+
throw err;
|
|
2103
|
+
}
|
|
2104
|
+
if (err instanceof ModelRelayError) {
|
|
2105
|
+
recordHttpMetrics(metrics, trace, start, void 0, {
|
|
2106
|
+
error: err,
|
|
2107
|
+
context
|
|
2108
|
+
});
|
|
2109
|
+
throw err;
|
|
2110
|
+
}
|
|
2111
|
+
const transportKind = classifyTransportErrorKind(
|
|
2112
|
+
err,
|
|
2113
|
+
connectTimedOut,
|
|
2114
|
+
requestTimedOut
|
|
2115
|
+
);
|
|
2116
|
+
const shouldRetry = retryCfg && isRetryableError(err, transportKind) && (method !== "POST" || retryCfg.retryPost) && attempt < attempts;
|
|
2117
|
+
if (!shouldRetry) {
|
|
2118
|
+
const retries = buildRetryMetadata(
|
|
2119
|
+
attempt,
|
|
2120
|
+
lastStatus,
|
|
2121
|
+
err instanceof Error ? err.message : String(err)
|
|
2122
|
+
);
|
|
2123
|
+
recordHttpMetrics(metrics, trace, start, retries, {
|
|
2124
|
+
error: err,
|
|
2125
|
+
context
|
|
2126
|
+
});
|
|
2127
|
+
throw toTransportError(err, transportKind, retries);
|
|
2128
|
+
}
|
|
2129
|
+
lastError = err;
|
|
2130
|
+
await backoff(attempt, retryCfg);
|
|
2131
|
+
} finally {
|
|
2132
|
+
if (connectTimer) {
|
|
2133
|
+
clearTimeout(connectTimer);
|
|
2134
|
+
}
|
|
2135
|
+
if (requestTimer) {
|
|
2136
|
+
clearTimeout(requestTimer);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
throw lastError instanceof Error ? lastError : new TransportError("request failed", {
|
|
2141
|
+
kind: "other",
|
|
2142
|
+
retries: buildRetryMetadata(attempts, lastStatus)
|
|
2143
|
+
});
|
|
1840
2144
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
if (!handler) {
|
|
1850
|
-
return {
|
|
1851
|
-
toolCallId: call.id,
|
|
1852
|
-
toolName,
|
|
1853
|
-
result: null,
|
|
1854
|
-
error: `Unknown tool: '${toolName}'. Available tools: ${this.getRegisteredTools().join(", ") || "none"}`
|
|
1855
|
-
};
|
|
2145
|
+
async json(path, options = {}) {
|
|
2146
|
+
const response = await this.request(path, {
|
|
2147
|
+
...options,
|
|
2148
|
+
raw: true,
|
|
2149
|
+
accept: options.accept || "application/json"
|
|
2150
|
+
});
|
|
2151
|
+
if (!response.ok) {
|
|
2152
|
+
throw await parseErrorResponse(response);
|
|
1856
2153
|
}
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
args = call.function?.arguments ? JSON.parse(call.function.arguments) : {};
|
|
1860
|
-
} catch (err) {
|
|
1861
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1862
|
-
return {
|
|
1863
|
-
toolCallId: call.id,
|
|
1864
|
-
toolName,
|
|
1865
|
-
result: null,
|
|
1866
|
-
error: `Invalid JSON in arguments: ${errorMessage}`,
|
|
1867
|
-
isRetryable: true
|
|
1868
|
-
};
|
|
2154
|
+
if (response.status === 204) {
|
|
2155
|
+
return void 0;
|
|
1869
2156
|
}
|
|
1870
2157
|
try {
|
|
1871
|
-
|
|
1872
|
-
return {
|
|
1873
|
-
toolCallId: call.id,
|
|
1874
|
-
toolName,
|
|
1875
|
-
result
|
|
1876
|
-
};
|
|
2158
|
+
return await response.json();
|
|
1877
2159
|
} catch (err) {
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
toolName,
|
|
1883
|
-
result: null,
|
|
1884
|
-
error: errorMessage,
|
|
1885
|
-
isRetryable
|
|
1886
|
-
};
|
|
2160
|
+
throw new APIError("failed to parse response JSON", {
|
|
2161
|
+
status: response.status,
|
|
2162
|
+
data: err
|
|
2163
|
+
});
|
|
1887
2164
|
}
|
|
1888
2165
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
*/
|
|
1894
|
-
async executeAll(calls) {
|
|
1895
|
-
return Promise.all(calls.map((call) => this.execute(call)));
|
|
2166
|
+
};
|
|
2167
|
+
function buildUrl(baseUrl, path) {
|
|
2168
|
+
if (/^https?:\/\//i.test(path)) {
|
|
2169
|
+
return path;
|
|
1896
2170
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
* Useful for appending to the conversation history.
|
|
1900
|
-
* @param results - Array of execution results
|
|
1901
|
-
* @returns Array of ChatMessage objects with role "tool"
|
|
1902
|
-
*/
|
|
1903
|
-
resultsToMessages(results) {
|
|
1904
|
-
return results.map((r) => {
|
|
1905
|
-
const content = r.error ? `Error: ${r.error}` : typeof r.result === "string" ? r.result : JSON.stringify(r.result);
|
|
1906
|
-
return toolResultMessage(r.toolCallId, content);
|
|
1907
|
-
});
|
|
2171
|
+
if (!path.startsWith("/")) {
|
|
2172
|
+
path = `/${path}`;
|
|
1908
2173
|
}
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
lines.push("");
|
|
1916
|
-
lines.push("Please correct the arguments and try again.");
|
|
2174
|
+
return `${baseUrl}${path}`;
|
|
2175
|
+
}
|
|
2176
|
+
function normalizeBaseUrl(value) {
|
|
2177
|
+
const trimmed = value.trim();
|
|
2178
|
+
if (trimmed.endsWith("/")) {
|
|
2179
|
+
return trimmed.slice(0, -1);
|
|
1917
2180
|
}
|
|
1918
|
-
return
|
|
2181
|
+
return trimmed;
|
|
1919
2182
|
}
|
|
1920
|
-
function
|
|
1921
|
-
return
|
|
2183
|
+
function isValidHttpUrl(value) {
|
|
2184
|
+
return /^https?:\/\//i.test(value);
|
|
1922
2185
|
}
|
|
1923
|
-
function
|
|
1924
|
-
|
|
2186
|
+
function normalizeRetryConfig(retry) {
|
|
2187
|
+
if (retry === false) return void 0;
|
|
2188
|
+
const cfg = retry || {};
|
|
2189
|
+
return {
|
|
2190
|
+
maxAttempts: Math.max(1, cfg.maxAttempts ?? 3),
|
|
2191
|
+
baseBackoffMs: Math.max(0, cfg.baseBackoffMs ?? 300),
|
|
2192
|
+
maxBackoffMs: Math.max(0, cfg.maxBackoffMs ?? 5e3),
|
|
2193
|
+
retryPost: cfg.retryPost ?? true
|
|
2194
|
+
};
|
|
1925
2195
|
}
|
|
1926
|
-
function
|
|
1927
|
-
|
|
2196
|
+
function shouldRetryStatus(status, method, retryPost) {
|
|
2197
|
+
if (status === 408 || status === 429) {
|
|
2198
|
+
return method !== "POST" || retryPost;
|
|
2199
|
+
}
|
|
2200
|
+
if (status >= 500 && status < 600) {
|
|
2201
|
+
return method !== "POST" || retryPost;
|
|
2202
|
+
}
|
|
2203
|
+
return false;
|
|
1928
2204
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
for (const result of retryableResults) {
|
|
1953
|
-
successfulResults.set(result.toolCallId, result);
|
|
1954
|
-
}
|
|
1955
|
-
return Array.from(successfulResults.values());
|
|
2205
|
+
function isRetryableError(err, kind) {
|
|
2206
|
+
if (!err) return false;
|
|
2207
|
+
if (kind === "timeout" || kind === "connect") return true;
|
|
2208
|
+
return err instanceof DOMException || err instanceof TypeError;
|
|
2209
|
+
}
|
|
2210
|
+
function backoff(attempt, cfg) {
|
|
2211
|
+
const exp = Math.max(0, attempt - 1);
|
|
2212
|
+
const base = cfg.baseBackoffMs * Math.pow(2, Math.min(exp, 10));
|
|
2213
|
+
const capped = Math.min(base, cfg.maxBackoffMs);
|
|
2214
|
+
const jitter = 0.5 + Math.random();
|
|
2215
|
+
const delay = Math.min(cfg.maxBackoffMs, capped * jitter);
|
|
2216
|
+
if (delay <= 0) return Promise.resolve();
|
|
2217
|
+
return new Promise((resolve) => setTimeout(resolve, delay));
|
|
2218
|
+
}
|
|
2219
|
+
function mergeSignals(...signals) {
|
|
2220
|
+
const active = signals.filter(Boolean);
|
|
2221
|
+
if (active.length === 0) return void 0;
|
|
2222
|
+
if (active.length === 1) return active[0];
|
|
2223
|
+
const controller = new AbortController();
|
|
2224
|
+
for (const src of active) {
|
|
2225
|
+
if (src.aborted) {
|
|
2226
|
+
controller.abort(src.reason);
|
|
2227
|
+
break;
|
|
1956
2228
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2229
|
+
src.addEventListener(
|
|
2230
|
+
"abort",
|
|
2231
|
+
() => controller.abort(src.reason),
|
|
2232
|
+
{ once: true }
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
return controller.signal;
|
|
2236
|
+
}
|
|
2237
|
+
function normalizeHeaders(headers) {
|
|
2238
|
+
if (!headers) return {};
|
|
2239
|
+
const normalized = {};
|
|
2240
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
2241
|
+
if (!key || !value) continue;
|
|
2242
|
+
const k = key.trim();
|
|
2243
|
+
const v = value.trim();
|
|
2244
|
+
if (k && v) {
|
|
2245
|
+
normalized[k] = v;
|
|
1964
2246
|
}
|
|
1965
|
-
currentCalls = newCalls;
|
|
1966
2247
|
}
|
|
1967
|
-
return
|
|
2248
|
+
return normalized;
|
|
2249
|
+
}
|
|
2250
|
+
function buildRetryMetadata(attempt, lastStatus, lastError) {
|
|
2251
|
+
if (!attempt || attempt <= 1) return void 0;
|
|
2252
|
+
return {
|
|
2253
|
+
attempts: attempt,
|
|
2254
|
+
lastStatus,
|
|
2255
|
+
lastError: typeof lastError === "string" ? lastError : lastError instanceof Error ? lastError.message : lastError ? String(lastError) : void 0
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
function classifyTransportErrorKind(err, connectTimedOut, requestTimedOut) {
|
|
2259
|
+
if (connectTimedOut) return "connect";
|
|
2260
|
+
if (requestTimedOut) return "timeout";
|
|
2261
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
2262
|
+
return requestTimedOut ? "timeout" : "request";
|
|
2263
|
+
}
|
|
2264
|
+
if (err instanceof TypeError) return "request";
|
|
2265
|
+
return "other";
|
|
2266
|
+
}
|
|
2267
|
+
function toTransportError(err, kind, retries) {
|
|
2268
|
+
const message = err instanceof Error ? err.message : typeof err === "string" ? err : "request failed";
|
|
2269
|
+
return new TransportError(message, { kind, retries, cause: err });
|
|
2270
|
+
}
|
|
2271
|
+
function recordHttpMetrics(metrics, trace, start, retries, info) {
|
|
2272
|
+
if (!metrics?.httpRequest && !trace?.requestFinish) return;
|
|
2273
|
+
const latencyMs = start ? Date.now() - start : 0;
|
|
2274
|
+
if (metrics?.httpRequest) {
|
|
2275
|
+
metrics.httpRequest({
|
|
2276
|
+
latencyMs,
|
|
2277
|
+
status: info.status,
|
|
2278
|
+
error: info.error ? String(info.error) : void 0,
|
|
2279
|
+
retries,
|
|
2280
|
+
context: info.context
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
trace?.requestFinish?.({
|
|
2284
|
+
context: info.context,
|
|
2285
|
+
status: info.status,
|
|
2286
|
+
error: info.error,
|
|
2287
|
+
retries,
|
|
2288
|
+
latencyMs
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
function withRequestId(context, headers) {
|
|
2292
|
+
const requestId = headers.get("X-ModelRelay-Chat-Request-Id") || headers.get("X-Request-Id") || context.requestId;
|
|
2293
|
+
if (!requestId) return context;
|
|
2294
|
+
return { ...context, requestId };
|
|
1968
2295
|
}
|
|
1969
2296
|
|
|
1970
2297
|
// src/index.ts
|
|
@@ -2022,12 +2349,13 @@ export {
|
|
|
2022
2349
|
DEFAULT_CLIENT_HEADER,
|
|
2023
2350
|
DEFAULT_CONNECT_TIMEOUT_MS,
|
|
2024
2351
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
2352
|
+
ErrorCodes,
|
|
2025
2353
|
ModelRelay,
|
|
2026
2354
|
ModelRelayError,
|
|
2027
|
-
|
|
2028
|
-
Providers,
|
|
2355
|
+
ResponseFormatTypes,
|
|
2029
2356
|
SDK_VERSION,
|
|
2030
2357
|
StopReasons,
|
|
2358
|
+
StructuredJSONStream,
|
|
2031
2359
|
TiersClient,
|
|
2032
2360
|
ToolArgsError,
|
|
2033
2361
|
ToolCallAccumulator,
|
|
@@ -2035,11 +2363,20 @@ export {
|
|
|
2035
2363
|
ToolRegistry,
|
|
2036
2364
|
ToolTypes,
|
|
2037
2365
|
TransportError,
|
|
2366
|
+
WebToolModes,
|
|
2038
2367
|
assistantMessageWithToolCalls,
|
|
2368
|
+
createAccessTokenAuth,
|
|
2369
|
+
createApiKeyAuth,
|
|
2370
|
+
createAssistantMessage,
|
|
2371
|
+
createFunctionCall,
|
|
2039
2372
|
createFunctionTool,
|
|
2040
2373
|
createFunctionToolFromSchema,
|
|
2041
2374
|
createRetryMessages,
|
|
2042
|
-
|
|
2375
|
+
createSystemMessage,
|
|
2376
|
+
createToolCall,
|
|
2377
|
+
createUsage,
|
|
2378
|
+
createUserMessage,
|
|
2379
|
+
createWebTool,
|
|
2043
2380
|
executeWithRetry,
|
|
2044
2381
|
firstToolCall,
|
|
2045
2382
|
formatToolErrorForModel,
|
|
@@ -2051,12 +2388,10 @@ export {
|
|
|
2051
2388
|
mergeTrace,
|
|
2052
2389
|
modelToString,
|
|
2053
2390
|
normalizeModelId,
|
|
2054
|
-
normalizeProvider,
|
|
2055
2391
|
normalizeStopReason,
|
|
2056
2392
|
parseErrorResponse,
|
|
2057
2393
|
parseToolArgs,
|
|
2058
2394
|
parseToolArgsRaw,
|
|
2059
|
-
providerToString,
|
|
2060
2395
|
respondToToolCall,
|
|
2061
2396
|
stopReasonToString,
|
|
2062
2397
|
toolChoiceAuto,
|