@modelrelay/sdk 0.7.0 → 0.14.1

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