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