@modelrelay/sdk 0.24.0 → 0.25.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/README.md CHANGED
@@ -205,6 +205,155 @@ const final = await stream.collect();
205
205
  console.log(final.items.length);
206
206
  ```
207
207
 
208
+ ### Type-safe structured outputs with Zod schemas
209
+
210
+ For automatic schema generation and validation, use `structured()` with Zod:
211
+
212
+ ```ts
213
+ import { ModelRelay } from "@modelrelay/sdk";
214
+ import { z } from "zod";
215
+
216
+ const mr = new ModelRelay({ key: "mr_sk_..." });
217
+
218
+ // Define your output type with Zod
219
+ const PersonSchema = z.object({
220
+ name: z.string(),
221
+ age: z.number(),
222
+ });
223
+
224
+ // structured() auto-generates JSON schema and validates responses
225
+ const result = await mr.chat.completions.structured(
226
+ PersonSchema,
227
+ {
228
+ model: "claude-sonnet-4-20250514",
229
+ messages: [{ role: "user", content: "Extract: John Doe is 30 years old" }],
230
+ },
231
+ { maxRetries: 2 } // Retry on validation failures
232
+ );
233
+
234
+ console.log(`Name: ${result.value.name}, Age: ${result.value.age}`);
235
+ console.log(`Succeeded on attempt ${result.attempts}`);
236
+ ```
237
+
238
+ #### Schema features
239
+
240
+ Zod schemas map to JSON Schema properties:
241
+
242
+ ```ts
243
+ const StatusSchema = z.object({
244
+ // Required string field
245
+ code: z.string(),
246
+
247
+ // Optional field (not in "required" array)
248
+ notes: z.string().optional(),
249
+
250
+ // Description for documentation
251
+ email: z.string().email().describe("User's email address"),
252
+
253
+ // Enum constraint
254
+ priority: z.enum(["low", "medium", "high"]),
255
+
256
+ // Nested objects are fully supported
257
+ address: z.object({
258
+ city: z.string(),
259
+ country: z.string(),
260
+ }),
261
+
262
+ // Arrays
263
+ tags: z.array(z.string()),
264
+ });
265
+ ```
266
+
267
+ #### Handling validation errors
268
+
269
+ When validation fails after all retries:
270
+
271
+ ```ts
272
+ import { StructuredExhaustedError } from "@modelrelay/sdk";
273
+
274
+ try {
275
+ const result = await mr.chat.completions.structured(
276
+ PersonSchema,
277
+ { model: "claude-sonnet-4-20250514", messages },
278
+ { maxRetries: 2 }
279
+ );
280
+ } catch (err) {
281
+ if (err instanceof StructuredExhaustedError) {
282
+ console.log(`Failed after ${err.allAttempts.length} attempts`);
283
+ for (const attempt of err.allAttempts) {
284
+ console.log(`Attempt ${attempt.attempt}: ${attempt.rawJson}`);
285
+ if (attempt.error.kind === "validation" && attempt.error.issues) {
286
+ for (const issue of attempt.error.issues) {
287
+ console.log(` - ${issue.path ?? "root"}: ${issue.message}`);
288
+ }
289
+ } else if (attempt.error.kind === "decode") {
290
+ console.log(` Decode error: ${attempt.error.message}`);
291
+ }
292
+ }
293
+ }
294
+ }
295
+ ```
296
+
297
+ #### Custom retry handlers
298
+
299
+ Customize retry behavior:
300
+
301
+ ```ts
302
+ import type { RetryHandler } from "@modelrelay/sdk";
303
+
304
+ const customHandler: RetryHandler = {
305
+ onValidationError(attempt, rawJson, error, messages) {
306
+ if (attempt >= 3) {
307
+ return null; // Stop retrying
308
+ }
309
+ return [
310
+ {
311
+ role: "user",
312
+ content: `Invalid response. Issues: ${JSON.stringify(error.issues)}. Try again.`,
313
+ },
314
+ ];
315
+ },
316
+ };
317
+
318
+ const result = await mr.chat.completions.structured(
319
+ PersonSchema,
320
+ { model: "claude-sonnet-4-20250514", messages },
321
+ { maxRetries: 3, retryHandler: customHandler }
322
+ );
323
+ ```
324
+
325
+ #### Streaming structured outputs
326
+
327
+ For streaming with Zod schema (no retries):
328
+
329
+ ```ts
330
+ const stream = await mr.chat.completions.streamStructured(
331
+ PersonSchema,
332
+ {
333
+ model: "claude-sonnet-4-20250514",
334
+ messages: [{ role: "user", content: "Extract: Jane, 25" }],
335
+ }
336
+ );
337
+
338
+ for await (const evt of stream) {
339
+ if (evt.type === "completion") {
340
+ console.log("Final:", evt.payload);
341
+ }
342
+ }
343
+ ```
344
+
345
+ #### Customer-attributed structured outputs
346
+
347
+ Works with customer-attributed requests too:
348
+
349
+ ```ts
350
+ const result = await mr.chat.forCustomer("customer-123").structured(
351
+ PersonSchema,
352
+ { messages: [{ role: "user", content: "Extract: John, 30" }] },
353
+ { maxRetries: 2 }
354
+ );
355
+ ```
356
+
208
357
  ### Telemetry & metrics hooks
209
358
 
210
359
  Provide lightweight callbacks to observe latency and usage without extra deps:
package/dist/index.cjs CHANGED
@@ -38,6 +38,8 @@ __export(index_exports, {
38
38
  ResponseFormatTypes: () => ResponseFormatTypes,
39
39
  SDK_VERSION: () => SDK_VERSION,
40
40
  StopReasons: () => StopReasons,
41
+ StructuredDecodeError: () => StructuredDecodeError,
42
+ StructuredExhaustedError: () => StructuredExhaustedError,
41
43
  StructuredJSONStream: () => StructuredJSONStream,
42
44
  TiersClient: () => TiersClient,
43
45
  ToolArgsError: () => ToolArgsError,
@@ -60,6 +62,7 @@ __export(index_exports, {
60
62
  createUsage: () => createUsage,
61
63
  createUserMessage: () => createUserMessage,
62
64
  createWebTool: () => createWebTool,
65
+ defaultRetryHandler: () => defaultRetryHandler,
63
66
  executeWithRetry: () => executeWithRetry,
64
67
  firstToolCall: () => firstToolCall,
65
68
  formatToolErrorForModel: () => formatToolErrorForModel,
@@ -80,12 +83,14 @@ __export(index_exports, {
80
83
  parseToolArgs: () => parseToolArgs,
81
84
  parseToolArgsRaw: () => parseToolArgsRaw,
82
85
  respondToToolCall: () => respondToToolCall,
86
+ responseFormatFromZod: () => responseFormatFromZod,
83
87
  stopReasonToString: () => stopReasonToString,
84
88
  toolChoiceAuto: () => toolChoiceAuto,
85
89
  toolChoiceNone: () => toolChoiceNone,
86
90
  toolChoiceRequired: () => toolChoiceRequired,
87
91
  toolResultMessage: () => toolResultMessage,
88
92
  tryParseToolArgs: () => tryParseToolArgs,
93
+ validateWithZod: () => validateWithZod,
89
94
  zodToJsonSchema: () => zodToJsonSchema
90
95
  });
91
96
  module.exports = __toCommonJS(index_exports);
@@ -432,7 +437,7 @@ function isTokenReusable(token) {
432
437
  // package.json
433
438
  var package_default = {
434
439
  name: "@modelrelay/sdk",
435
- version: "0.24.0",
440
+ version: "0.25.1",
436
441
  description: "TypeScript SDK for the ModelRelay API",
437
442
  type: "module",
438
443
  main: "dist/index.cjs",
@@ -1192,6 +1197,58 @@ async function executeWithRetry(registry, toolCalls, options = {}) {
1192
1197
  return Array.from(successfulResults.values());
1193
1198
  }
1194
1199
 
1200
+ // src/structured.ts
1201
+ var StructuredDecodeError = class extends Error {
1202
+ constructor(message, rawJson, attempt) {
1203
+ super(`structured output decode error (attempt ${attempt}): ${message}`);
1204
+ this.name = "StructuredDecodeError";
1205
+ this.rawJson = rawJson;
1206
+ this.attempt = attempt;
1207
+ }
1208
+ };
1209
+ var StructuredExhaustedError = class extends Error {
1210
+ constructor(lastRawJson, allAttempts, finalError) {
1211
+ const errorMsg = finalError.kind === "decode" ? finalError.message : finalError.issues.map((i) => i.message).join("; ");
1212
+ super(
1213
+ `structured output failed after ${allAttempts.length} attempts: ${errorMsg}`
1214
+ );
1215
+ this.name = "StructuredExhaustedError";
1216
+ this.lastRawJson = lastRawJson;
1217
+ this.allAttempts = allAttempts;
1218
+ this.finalError = finalError;
1219
+ }
1220
+ };
1221
+ var defaultRetryHandler = {
1222
+ onValidationError(_attempt, _rawJson, error, _originalMessages) {
1223
+ const errorMsg = error.kind === "decode" ? error.message : error.issues.map((i) => `${i.path ?? ""}: ${i.message}`).join("; ");
1224
+ return [
1225
+ {
1226
+ role: "user",
1227
+ content: `The previous response did not match the expected schema. Error: ${errorMsg}. Please provide a response that matches the schema exactly.`
1228
+ }
1229
+ ];
1230
+ }
1231
+ };
1232
+ function responseFormatFromZod(schema, name = "response") {
1233
+ const jsonSchema = zodToJsonSchema(schema);
1234
+ return {
1235
+ type: "json_schema",
1236
+ json_schema: {
1237
+ name,
1238
+ schema: jsonSchema,
1239
+ strict: true
1240
+ }
1241
+ };
1242
+ }
1243
+ function validateWithZod(schema, data) {
1244
+ const result = schema.safeParse(data);
1245
+ if (result.success) {
1246
+ return { success: true, data: result.data };
1247
+ }
1248
+ const errorMsg = result.error && typeof result.error === "object" && "message" in result.error ? String(result.error.message) : "validation failed";
1249
+ return { success: false, error: errorMsg };
1250
+ }
1251
+
1195
1252
  // src/chat.ts
1196
1253
  var CUSTOMER_ID_HEADER = "X-ModelRelay-Customer-Id";
1197
1254
  var REQUEST_ID_HEADER = "X-ModelRelay-Chat-Request-Id";
@@ -1389,6 +1446,158 @@ var ChatCompletionsClient = class {
1389
1446
  trace
1390
1447
  );
1391
1448
  }
1449
+ /**
1450
+ * Send a structured output request with a Zod schema.
1451
+ *
1452
+ * Auto-generates JSON schema from the Zod schema, validates the response,
1453
+ * and retries on validation failure if configured.
1454
+ *
1455
+ * @param schema - A Zod schema defining the expected response structure
1456
+ * @param params - Chat completion parameters (excluding responseFormat)
1457
+ * @param options - Request options including retry configuration
1458
+ * @returns A typed result with the parsed value
1459
+ *
1460
+ * @example
1461
+ * ```typescript
1462
+ * import { z } from 'zod';
1463
+ *
1464
+ * const PersonSchema = z.object({
1465
+ * name: z.string(),
1466
+ * age: z.number(),
1467
+ * });
1468
+ *
1469
+ * const result = await client.chat.completions.structured(
1470
+ * PersonSchema,
1471
+ * { model: "claude-sonnet-4-20250514", messages: [...] },
1472
+ * { maxRetries: 2 }
1473
+ * );
1474
+ * ```
1475
+ */
1476
+ async structured(schema, params, options = {}) {
1477
+ const {
1478
+ maxRetries = 0,
1479
+ retryHandler = defaultRetryHandler,
1480
+ schemaName,
1481
+ ...requestOptions
1482
+ } = options;
1483
+ const responseFormat = responseFormatFromZod(schema, schemaName);
1484
+ const fullParams = {
1485
+ ...params,
1486
+ responseFormat,
1487
+ stream: false
1488
+ };
1489
+ let messages = [...params.messages];
1490
+ const attempts = [];
1491
+ const maxAttempts = maxRetries + 1;
1492
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1493
+ const response = await this.create(
1494
+ { ...fullParams, messages },
1495
+ { ...requestOptions, stream: false }
1496
+ );
1497
+ const rawJson = response.content.join("");
1498
+ const requestId = response.requestId;
1499
+ try {
1500
+ const parsed = JSON.parse(rawJson);
1501
+ const validated = validateWithZod(schema, parsed);
1502
+ if (validated.success) {
1503
+ return {
1504
+ value: validated.data,
1505
+ attempts: attempt,
1506
+ requestId
1507
+ };
1508
+ }
1509
+ const error = {
1510
+ kind: "validation",
1511
+ issues: [{ message: validated.error }]
1512
+ };
1513
+ attempts.push({ attempt, rawJson, error });
1514
+ if (attempt >= maxAttempts) {
1515
+ throw new StructuredExhaustedError(rawJson, attempts, error);
1516
+ }
1517
+ const retryMessages = retryHandler.onValidationError(
1518
+ attempt,
1519
+ rawJson,
1520
+ error,
1521
+ params.messages
1522
+ );
1523
+ if (!retryMessages) {
1524
+ throw new StructuredExhaustedError(rawJson, attempts, error);
1525
+ }
1526
+ messages = [
1527
+ ...params.messages,
1528
+ { role: "assistant", content: rawJson },
1529
+ ...retryMessages
1530
+ ];
1531
+ } catch (e) {
1532
+ if (e instanceof StructuredExhaustedError) {
1533
+ throw e;
1534
+ }
1535
+ const error = {
1536
+ kind: "decode",
1537
+ message: e instanceof Error ? e.message : String(e)
1538
+ };
1539
+ attempts.push({ attempt, rawJson, error });
1540
+ if (attempt >= maxAttempts) {
1541
+ throw new StructuredExhaustedError(rawJson, attempts, error);
1542
+ }
1543
+ const retryMessages = retryHandler.onValidationError(
1544
+ attempt,
1545
+ rawJson,
1546
+ error,
1547
+ params.messages
1548
+ );
1549
+ if (!retryMessages) {
1550
+ throw new StructuredExhaustedError(rawJson, attempts, error);
1551
+ }
1552
+ messages = [
1553
+ ...params.messages,
1554
+ { role: "assistant", content: rawJson },
1555
+ ...retryMessages
1556
+ ];
1557
+ }
1558
+ }
1559
+ throw new Error(
1560
+ `Internal error: structured output loop exited unexpectedly after ${maxAttempts} attempts (this is a bug, please report it)`
1561
+ );
1562
+ }
1563
+ /**
1564
+ * Stream structured output with a Zod schema.
1565
+ *
1566
+ * Auto-generates JSON schema from the Zod schema. Note that streaming
1567
+ * does not support retries - for retry behavior, use `structured()`.
1568
+ *
1569
+ * @param schema - A Zod schema defining the expected response structure
1570
+ * @param params - Chat completion parameters (excluding responseFormat)
1571
+ * @param options - Request options
1572
+ * @returns A structured JSON stream
1573
+ *
1574
+ * @example
1575
+ * ```typescript
1576
+ * import { z } from 'zod';
1577
+ *
1578
+ * const PersonSchema = z.object({
1579
+ * name: z.string(),
1580
+ * age: z.number(),
1581
+ * });
1582
+ *
1583
+ * const stream = await client.chat.completions.streamStructured(
1584
+ * PersonSchema,
1585
+ * { model: "claude-sonnet-4-20250514", messages: [...] },
1586
+ * );
1587
+ *
1588
+ * for await (const event of stream) {
1589
+ * console.log(event.type, event.payload);
1590
+ * }
1591
+ * ```
1592
+ */
1593
+ async streamStructured(schema, params, options = {}) {
1594
+ const { schemaName, ...requestOptions } = options;
1595
+ const responseFormat = responseFormatFromZod(schema, schemaName);
1596
+ return this.streamJSON(
1597
+ { ...params, responseFormat },
1598
+ requestOptions
1599
+ );
1600
+ }
1392
1601
  };
1393
1602
  var CustomerChatClient = class {
1394
1603
  constructor(http, auth, customerId, defaultMetadata, metrics, trace) {
@@ -1553,6 +1762,123 @@ var CustomerChatClient = class {
1553
1762
  trace
1554
1763
  );
1555
1764
  }
1765
+ /**
1766
+ * Send a structured output request with a Zod schema for customer-attributed calls.
1767
+ *
1768
+ * Auto-generates JSON schema from the Zod schema, validates the response,
1769
+ * and retries on validation failure if configured.
1770
+ *
1771
+ * @param schema - A Zod schema defining the expected response structure
1772
+ * @param params - Customer chat parameters (excluding responseFormat)
1773
+ * @param options - Request options including retry configuration
1774
+ * @returns A typed result with the parsed value
1775
+ */
1776
+ async structured(schema, params, options = {}) {
1777
+ const {
1778
+ maxRetries = 0,
1779
+ retryHandler = defaultRetryHandler,
1780
+ schemaName,
1781
+ ...requestOptions
1782
+ } = options;
1783
+ const responseFormat = responseFormatFromZod(schema, schemaName);
1784
+ const fullParams = {
1785
+ ...params,
1786
+ responseFormat,
1787
+ stream: false
1788
+ };
1789
+ let messages = [...params.messages];
1790
+ const attempts = [];
1791
+ const maxAttempts = maxRetries + 1;
1792
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1793
+ const response = await this.create(
1794
+ { ...fullParams, messages },
1795
+ { ...requestOptions, stream: false }
1796
+ );
1797
+ const rawJson = response.content.join("");
1798
+ const requestId = response.requestId;
1799
+ try {
1800
+ const parsed = JSON.parse(rawJson);
1801
+ const validated = validateWithZod(schema, parsed);
1802
+ if (validated.success) {
1803
+ return {
1804
+ value: validated.data,
1805
+ attempts: attempt,
1806
+ requestId
1807
+ };
1808
+ }
1809
+ const error = {
1810
+ kind: "validation",
1811
+ issues: [{ message: validated.error }]
1812
+ };
1813
+ attempts.push({ attempt, rawJson, error });
1814
+ if (attempt >= maxAttempts) {
1815
+ throw new StructuredExhaustedError(rawJson, attempts, error);
1816
+ }
1817
+ const retryMessages = retryHandler.onValidationError(
1818
+ attempt,
1819
+ rawJson,
1820
+ error,
1821
+ params.messages
1822
+ );
1823
+ if (!retryMessages) {
1824
+ throw new StructuredExhaustedError(rawJson, attempts, error);
1825
+ }
1826
+ messages = [
1827
+ ...params.messages,
1828
+ { role: "assistant", content: rawJson },
1829
+ ...retryMessages
1830
+ ];
1831
+ } catch (e) {
1832
+ if (e instanceof StructuredExhaustedError) {
1833
+ throw e;
1834
+ }
1835
+ const error = {
1836
+ kind: "decode",
1837
+ message: e instanceof Error ? e.message : String(e)
1838
+ };
1839
+ attempts.push({ attempt, rawJson, error });
1840
+ if (attempt >= maxAttempts) {
1841
+ throw new StructuredExhaustedError(rawJson, attempts, error);
1842
+ }
1843
+ const retryMessages = retryHandler.onValidationError(
1844
+ attempt,
1845
+ rawJson,
1846
+ error,
1847
+ params.messages
1848
+ );
1849
+ if (!retryMessages) {
1850
+ throw new StructuredExhaustedError(rawJson, attempts, error);
1851
+ }
1852
+ messages = [
1853
+ ...params.messages,
1854
+ { role: "assistant", content: rawJson },
1855
+ ...retryMessages
1856
+ ];
1857
+ }
1858
+ }
1859
+ throw new Error(
1860
+ `Internal error: structured output loop exited unexpectedly after ${maxAttempts} attempts (this is a bug, please report it)`
1861
+ );
1862
+ }
1863
+ /**
1864
+ * Stream structured output with a Zod schema for customer-attributed calls.
1865
+ *
1866
+ * Auto-generates JSON schema from the Zod schema. Note that streaming
1867
+ * does not support retries - for retry behavior, use `structured()`.
1868
+ *
1869
+ * @param schema - A Zod schema defining the expected response structure
1870
+ * @param params - Customer chat parameters (excluding responseFormat)
1871
+ * @param options - Request options
1872
+ * @returns A structured JSON stream
1873
+ */
1874
+ async streamStructured(schema, params, options = {}) {
1875
+ const { schemaName, ...requestOptions } = options;
1876
+ const responseFormat = responseFormatFromZod(schema, schemaName);
1877
+ return this.streamJSON(
1878
+ { ...params, responseFormat },
1879
+ requestOptions
1880
+ );
1881
+ }
1556
1882
  };
1557
1883
  var ChatCompletionsStream = class {
1558
1884
  constructor(response, requestId, context, metrics, trace) {
@@ -1572,7 +1898,13 @@ var ChatCompletionsStream = class {
1572
1898
  this.closed = true;
1573
1899
  try {
1574
1900
  await this.response.body?.cancel(reason);
1575
- } catch {
1901
+ } catch (err) {
1902
+ if (this.trace?.streamError) {
1903
+ this.trace.streamError({
1904
+ context: this.context,
1905
+ error: err instanceof Error ? err : new Error(String(err))
1906
+ });
1907
+ }
1576
1908
  }
1577
1909
  }
1578
1910
  async *[Symbol.asyncIterator]() {
@@ -1671,7 +2003,13 @@ var StructuredJSONStream = class {
1671
2003
  this.closed = true;
1672
2004
  try {
1673
2005
  await this.response.body?.cancel(reason);
1674
- } catch {
2006
+ } catch (err) {
2007
+ if (this.trace?.streamError) {
2008
+ this.trace.streamError({
2009
+ context: this.context,
2010
+ error: err instanceof Error ? err : new Error(String(err))
2011
+ });
2012
+ }
1675
2013
  }
1676
2014
  }
1677
2015
  async *[Symbol.asyncIterator]() {
@@ -1872,7 +2210,7 @@ function mapChatEvent(raw, requestId) {
1872
2210
  if (raw.data) {
1873
2211
  try {
1874
2212
  parsed = JSON.parse(raw.data);
1875
- } catch {
2213
+ } catch (err) {
1876
2214
  parsed = raw.data;
1877
2215
  }
1878
2216
  }
@@ -2785,6 +3123,8 @@ function resolveBaseUrl(override) {
2785
3123
  ResponseFormatTypes,
2786
3124
  SDK_VERSION,
2787
3125
  StopReasons,
3126
+ StructuredDecodeError,
3127
+ StructuredExhaustedError,
2788
3128
  StructuredJSONStream,
2789
3129
  TiersClient,
2790
3130
  ToolArgsError,
@@ -2807,6 +3147,7 @@ function resolveBaseUrl(override) {
2807
3147
  createUsage,
2808
3148
  createUserMessage,
2809
3149
  createWebTool,
3150
+ defaultRetryHandler,
2810
3151
  executeWithRetry,
2811
3152
  firstToolCall,
2812
3153
  formatToolErrorForModel,
@@ -2827,11 +3168,13 @@ function resolveBaseUrl(override) {
2827
3168
  parseToolArgs,
2828
3169
  parseToolArgsRaw,
2829
3170
  respondToToolCall,
3171
+ responseFormatFromZod,
2830
3172
  stopReasonToString,
2831
3173
  toolChoiceAuto,
2832
3174
  toolChoiceNone,
2833
3175
  toolChoiceRequired,
2834
3176
  toolResultMessage,
2835
3177
  tryParseToolArgs,
3178
+ validateWithZod,
2836
3179
  zodToJsonSchema
2837
3180
  });