@modelrelay/sdk 0.23.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
@@ -82,12 +82,53 @@ const stream = await mr.chat.completions.create(
82
82
  );
83
83
  ```
84
84
 
85
- ### Typed models and stop reasons
85
+ ### Typed models, stop reasons, and message roles
86
86
 
87
87
  - Models are plain strings (e.g., `"gpt-4o"`), so new models do not require SDK updates.
88
88
  - Stop reasons are parsed into the `StopReason` union (e.g., `StopReasons.EndTurn`); unknown values surface as `{ other: "<raw>" }`.
89
+ - Message roles use a typed union (`MessageRole`) with constants available via `MessageRoles`.
89
90
  - Usage backfills `totalTokens` when the backend omits it, ensuring consistent accounting.
90
91
 
92
+ ```ts
93
+ import { MessageRoles } from "@modelrelay/sdk";
94
+
95
+ // Use typed role constants
96
+ const messages = [
97
+ { role: MessageRoles.System, content: "You are helpful." },
98
+ { role: MessageRoles.User, content: "Hello!" },
99
+ ];
100
+
101
+ // Available roles: User, Assistant, System, Tool
102
+ ```
103
+
104
+ ### Customer-attributed requests
105
+
106
+ For customer-attributed requests, the customer's tier determines which model to use.
107
+ Use `forCustomer()` instead of providing a model:
108
+
109
+ ```ts
110
+ // Customer-attributed: tier determines model, no model parameter needed
111
+ const stream = await mr.chat.forCustomer("customer-123").create({
112
+ messages: [{ role: "user", content: "Hello!" }]
113
+ });
114
+
115
+ for await (const event of stream) {
116
+ if (event.type === "message_delta" && event.textDelta) {
117
+ console.log(event.textDelta);
118
+ }
119
+ }
120
+
121
+ // Non-streaming
122
+ const completion = await mr.chat.forCustomer("customer-123").create(
123
+ { messages: [{ role: "user", content: "Hello!" }] },
124
+ { stream: false }
125
+ );
126
+ ```
127
+
128
+ This provides compile-time separation between:
129
+ - **Direct/PAYGO requests** (`chat.completions.create({ model, ... })`) — model is required
130
+ - **Customer-attributed requests** (`chat.forCustomer(id).create(...)`) — tier determines model
131
+
91
132
  ### Structured outputs (`response_format`)
92
133
 
93
134
  Request structured JSON instead of free-form text when the backend supports it:
@@ -164,6 +205,155 @@ const final = await stream.collect();
164
205
  console.log(final.items.length);
165
206
  ```
166
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
+
167
357
  ### Telemetry & metrics hooks
168
358
 
169
359
  Provide lightweight callbacks to observe latency and usage without extra deps: