@modelrelay/sdk 1.3.3 → 1.10.3

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.
Files changed (6) hide show
  1. package/README.md +263 -141
  2. package/dist/index.cjs +3476 -1008
  3. package/dist/index.d.cts +2199 -321
  4. package/dist/index.d.ts +2199 -321
  5. package/dist/index.js +3441 -1008
  6. package/package.json +15 -3
package/README.md CHANGED
@@ -1,40 +1,220 @@
1
1
  # ModelRelay TypeScript SDK
2
2
 
3
- The ModelRelay TypeScript SDK is a **responses-first**, **streaming-first** client for building cross-provider LLM features without committing to any single vendor API.
4
-
5
- It's designed to feel great in TypeScript:
6
- - One fluent builder for **streaming/non-streaming**, **text/structured**, and **customer-attributed** requests
7
- - Structured outputs powered by Zod schemas with validation and retry
8
- - A practical tool-use toolkit for "LLM + tools" apps
9
-
10
3
  ```bash
11
- npm install @modelrelay/sdk
12
- # or
13
4
  bun add @modelrelay/sdk
14
5
  ```
15
6
 
16
- ## Quick Start
7
+ ## Token Providers (Automatic Bearer Auth)
8
+
9
+ Use token providers when you want the SDK to automatically obtain/refresh **bearer tokens** for data-plane calls like `/responses` and `/runs`.
10
+
11
+ ### OIDC id_token → customer bearer token (exchange)
12
+
13
+ ```ts
14
+ import { ModelRelay, OIDCExchangeTokenProvider, parseSecretKey } from "@modelrelay/sdk";
15
+
16
+ const tokenProvider = new OIDCExchangeTokenProvider({
17
+ apiKey: parseSecretKey(process.env.MODELRELAY_API_KEY!),
18
+ idTokenProvider: async () => {
19
+ // Return an OIDC id_token from your auth system (web login, device flow, etc).
20
+ return process.env.OIDC_ID_TOKEN!;
21
+ },
22
+ });
23
+
24
+ const mr = new ModelRelay({ tokenProvider });
25
+ ```
26
+
27
+ If you need an `id_token` in a CLI-like context, you can use the OAuth device flow helper:
28
+
29
+ ```ts
30
+ import { runOAuthDeviceFlowForIDToken } from "@modelrelay/sdk";
31
+
32
+ const idToken = await runOAuthDeviceFlowForIDToken({
33
+ deviceAuthorizationEndpoint: "https://issuer.example.com/oauth/device/code",
34
+ tokenEndpoint: "https://issuer.example.com/oauth/token",
35
+ clientId: "your-client-id",
36
+ scope: "openid email profile",
37
+ onUserCode: ({ verificationUri, userCode }) => {
38
+ console.log(`Open ${verificationUri} and enter code: ${userCode}`);
39
+ },
40
+ });
41
+ ```
42
+
43
+ ### Secret key → customer bearer token (mint)
44
+
45
+ ```ts
46
+ import { CustomerTokenProvider, ModelRelay } from "@modelrelay/sdk";
47
+
48
+ const tokenProvider = new CustomerTokenProvider({
49
+ secretKey: process.env.MODELRELAY_API_KEY!,
50
+ request: { customerId: "customer_..." },
51
+ });
52
+
53
+ const mr = new ModelRelay({ tokenProvider });
54
+ ```
55
+
56
+ ## Streaming Responses
17
57
 
18
58
  ```ts
19
59
  import { ModelRelay } from "@modelrelay/sdk";
20
60
 
21
- const mr = ModelRelay.fromSecretKey(process.env.MODELRELAY_API_KEY!);
61
+ const mr = ModelRelay.fromSecretKey("mr_sk_...");
62
+
63
+ const req = mr.responses
64
+ .new()
65
+ .model("claude-sonnet-4-20250514")
66
+ .user("Hello")
67
+ .build();
22
68
 
23
- const response = await mr.responses.create(
24
- mr.responses
25
- .new()
26
- .model("claude-sonnet-4-20250514")
27
- .system("Answer concisely.")
28
- .user("Write one line about TypeScript.")
29
- .build()
69
+ const stream = await mr.responses.stream(req);
70
+
71
+ for await (const event of stream) {
72
+ if (event.type === "message_delta" && event.textDelta) {
73
+ process.stdout.write(event.textDelta);
74
+ }
75
+ }
76
+ ```
77
+
78
+ ## Customer-Scoped Convenience
79
+
80
+ ```ts
81
+ import { ModelRelay } from "@modelrelay/sdk";
82
+
83
+ const mr = ModelRelay.fromSecretKey("mr_sk_...");
84
+ const customer = mr.forCustomer("customer_abc123");
85
+
86
+ const text = await customer.responses.text(
87
+ "You are a helpful assistant.",
88
+ "Summarize Q4 results",
30
89
  );
90
+ ```
91
+
92
+ You can also stream structured JSON for a specific customer:
93
+
94
+ ```ts
95
+ import { z } from "zod";
96
+ import { ModelRelay, outputFormatFromZod } from "@modelrelay/sdk";
31
97
 
32
- console.log(response.text());
98
+ const mr = ModelRelay.fromSecretKey("mr_sk_...");
99
+ const customer = mr.forCustomer("customer_abc123");
100
+
101
+ const schema = z.object({
102
+ summary: z.string(),
103
+ highlights: z.array(z.string()),
104
+ });
105
+
106
+ const req = customer.responses
107
+ .new()
108
+ .outputFormat(outputFormatFromZod(schema))
109
+ .system("You are a helpful assistant.")
110
+ .user("Summarize Q4 results")
111
+ .build();
112
+
113
+ const stream = await customer.responses.streamJSON<z.infer<typeof schema>>(req);
114
+ for await (const event of stream) {
115
+ if (event.type === "completion") {
116
+ console.log(event.payload);
117
+ }
118
+ }
119
+ ```
120
+
121
+ You can also pass a single object to `textForCustomer`:
122
+
123
+ ```ts
124
+ const text = await mr.responses.textForCustomer({
125
+ customerId: "customer_abc123",
126
+ system: "You are a helpful assistant.",
127
+ user: "Summarize Q4 results",
128
+ });
33
129
  ```
34
130
 
131
+ ## Workflow Runs (workflow.v0)
132
+
133
+ ```ts
134
+ import {
135
+ ModelRelay,
136
+ type LLMResponsesBindingV0,
137
+ parseNodeId,
138
+ parseOutputName,
139
+ parseSecretKey,
140
+ workflowV0,
141
+ } from "@modelrelay/sdk";
142
+
143
+ const mr = new ModelRelay({ key: parseSecretKey("mr_sk_...") });
144
+
145
+ const spec = workflowV0()
146
+ .name("multi_agent_v0_example")
147
+ .execution({ max_parallelism: 3, node_timeout_ms: 20_000, run_timeout_ms: 30_000 })
148
+ .llmResponses(parseNodeId("agent_a"), {
149
+ model: "claude-sonnet-4-20250514",
150
+ input: [
151
+ { type: "message", role: "system", content: [{ type: "text", text: "You are Agent A." }] },
152
+ { type: "message", role: "user", content: [{ type: "text", text: "Write 3 ideas for a landing page." }] },
153
+ ],
154
+ })
155
+ .llmResponses(parseNodeId("agent_b"), {
156
+ model: "claude-sonnet-4-20250514",
157
+ input: [
158
+ { type: "message", role: "system", content: [{ type: "text", text: "You are Agent B." }] },
159
+ { type: "message", role: "user", content: [{ type: "text", text: "Write 3 objections a user might have." }] },
160
+ ],
161
+ })
162
+ .llmResponses(parseNodeId("agent_c"), {
163
+ model: "claude-sonnet-4-20250514",
164
+ input: [
165
+ { type: "message", role: "system", content: [{ type: "text", text: "You are Agent C." }] },
166
+ { type: "message", role: "user", content: [{ type: "text", text: "Write 3 alternative headlines." }] },
167
+ ],
168
+ })
169
+ .joinAll(parseNodeId("join"))
170
+ .llmResponses(
171
+ parseNodeId("aggregate"),
172
+ {
173
+ model: "claude-sonnet-4-20250514",
174
+ input: [
175
+ {
176
+ type: "message",
177
+ role: "system",
178
+ content: [{ type: "text", text: "Synthesize the best answer from the following agent outputs (JSON)." }],
179
+ },
180
+ { type: "message", role: "user", content: [{ type: "text", text: "" }] }, // overwritten by bindings
181
+ ],
182
+ },
183
+ {
184
+ // Bind the join output into the aggregator prompt (fan-in).
185
+ bindings: [
186
+ {
187
+ from: parseNodeId("join"),
188
+ to: "/input/1/content/0/text",
189
+ encoding: "json_string",
190
+ } satisfies LLMResponsesBindingV0,
191
+ ],
192
+ },
193
+ )
194
+ .edge(parseNodeId("agent_a"), parseNodeId("join"))
195
+ .edge(parseNodeId("agent_b"), parseNodeId("join"))
196
+ .edge(parseNodeId("agent_c"), parseNodeId("join"))
197
+ .edge(parseNodeId("join"), parseNodeId("aggregate"))
198
+ .output(parseOutputName("result"), parseNodeId("aggregate"))
199
+ .build();
200
+
201
+ const { run_id } = await mr.runs.create(spec);
202
+
203
+ const events = await mr.runs.events(run_id);
204
+ for await (const ev of events) {
205
+ if (ev.type === "run_completed") {
206
+ const status = await mr.runs.get(run_id);
207
+ console.log("outputs:", status.outputs);
208
+ console.log("cost_summary:", status.cost_summary);
209
+ }
210
+ }
211
+ ```
212
+
213
+ See the full example in `sdk/ts/examples/workflows_multi_agent.ts`.
214
+
35
215
  ## Chat-Like Text Helpers
36
216
 
37
- For the most common path (**system + user → assistant text**), use the built-in convenience:
217
+ For the most common path (**system + user → assistant text**):
38
218
 
39
219
  ```ts
40
220
  const text = await mr.responses.text(
@@ -48,16 +228,14 @@ console.log(text);
48
228
  For customer-attributed requests where the backend selects the model:
49
229
 
50
230
  ```ts
51
- const customer = mr.forCustomer("customer-123");
52
- const text = await customer.responses.text(
53
- "You are a helpful assistant.",
54
- "Summarize Q4 results",
231
+ const text = await mr.responses.textForCustomer(
232
+ "customer-123",
233
+ "Answer concisely.",
234
+ "Say hi.",
55
235
  );
56
236
  ```
57
237
 
58
- ## Streaming
59
-
60
- Stream text deltas for real-time output:
238
+ To stream only message text deltas:
61
239
 
62
240
  ```ts
63
241
  const deltas = await mr.responses.streamTextDeltas(
@@ -70,31 +248,14 @@ for await (const delta of deltas) {
70
248
  }
71
249
  ```
72
250
 
73
- For full control, stream typed events:
74
-
75
- ```ts
76
- const req = mr.responses
77
- .new()
78
- .model("claude-sonnet-4-20250514")
79
- .user("Hello")
80
- .build();
81
-
82
- const stream = await mr.responses.stream(req);
83
-
84
- for await (const event of stream) {
85
- if (event.type === "message_delta" && event.textDelta) {
86
- process.stdout.write(event.textDelta);
87
- }
88
- }
89
- ```
90
-
91
251
  ## Structured Outputs with Zod
92
252
 
93
- Get typed, validated responses from the model:
94
-
95
253
  ```ts
254
+ import { ModelRelay, parseSecretKey } from "@modelrelay/sdk";
96
255
  import { z } from "zod";
97
256
 
257
+ const mr = new ModelRelay({ key: parseSecretKey("mr_sk_...") });
258
+
98
259
  const Person = z.object({
99
260
  name: z.string(),
100
261
  age: z.number(),
@@ -109,11 +270,16 @@ const result = await mr.responses.structured(
109
270
  console.log(result.value); // { name: "John Doe", age: 30 }
110
271
  ```
111
272
 
112
- ### Streaming Structured Outputs
273
+ ## Streaming Structured Outputs
113
274
 
114
275
  Build progressive UIs that render fields as they complete:
115
276
 
116
277
  ```ts
278
+ import { ModelRelay, parseSecretKey } from "@modelrelay/sdk";
279
+ import { z } from "zod";
280
+
281
+ const mr = new ModelRelay({ key: parseSecretKey("mr_sk_...") });
282
+
117
283
  const Article = z.object({
118
284
  title: z.string(),
119
285
  summary: z.string(),
@@ -126,12 +292,15 @@ const stream = await mr.responses.streamStructured(
126
292
  );
127
293
 
128
294
  for await (const event of stream) {
295
+ // Render fields as soon as they're complete
129
296
  if (event.completeFields.has("title")) {
130
297
  renderTitle(event.payload.title); // Safe to display
131
298
  }
132
299
  if (event.completeFields.has("summary")) {
133
300
  renderSummary(event.payload.summary);
134
301
  }
302
+
303
+ // Show streaming preview of incomplete fields
135
304
  if (!event.completeFields.has("body")) {
136
305
  renderBodyPreview(event.payload.body + "▋");
137
306
  }
@@ -140,7 +309,7 @@ for await (const event of stream) {
140
309
 
141
310
  ## Customer-Attributed Requests
142
311
 
143
- For metered billing, use `customerId()`. The customer's tier determines the model, so `model()` can be omitted:
312
+ For metered billing, use `customerId()` the customer's subscription tier determines the model and `model` can be omitted:
144
313
 
145
314
  ```ts
146
315
  const req = mr.responses
@@ -152,122 +321,64 @@ const req = mr.responses
152
321
  const stream = await mr.responses.stream(req);
153
322
  ```
154
323
 
155
- Or use the convenience method:
156
-
157
- ```ts
158
- const text = await mr.responses.textForCustomer(
159
- "customer-123",
160
- "Answer concisely.",
161
- "Say hi.",
162
- );
163
- ```
164
-
165
324
  ## Customer Management (Backend)
166
325
 
167
326
  ```ts
168
327
  // Create/update customer
169
328
  const customer = await mr.customers.upsert({
170
- tier_id: "tier-uuid",
171
329
  external_id: "your-user-id",
172
330
  email: "user@example.com",
173
331
  });
174
332
 
175
333
  // Create checkout session for subscription billing
176
- const session = await mr.customers.createCheckoutSession(customer.id, {
334
+ const session = await mr.customers.subscribe(customer.customer.id, {
335
+ tier_id: "tier-uuid",
177
336
  success_url: "https://myapp.com/success",
178
337
  cancel_url: "https://myapp.com/cancel",
179
338
  });
180
339
 
181
340
  // Check subscription status
182
- const status = await mr.customers.getSubscription(customer.id);
341
+ const status = await mr.customers.getSubscription(customer.customer.id);
183
342
  ```
184
343
 
185
- ## Workflow Runs
344
+ ## Error Handling
186
345
 
187
- Build multi-agent workflows with parallel execution:
346
+ Errors are typed so callers can branch cleanly:
188
347
 
189
348
  ```ts
190
- import { workflowV0, parseNodeId, parseOutputName, type LLMResponsesBindingV0 } from "@modelrelay/sdk";
191
-
192
- const spec = workflowV0()
193
- .name("multi_agent_example")
194
- .execution({ max_parallelism: 3, node_timeout_ms: 20_000, run_timeout_ms: 30_000 })
195
- .llmResponses(parseNodeId("agent_a"), {
196
- model: "claude-sonnet-4-20250514",
197
- input: [
198
- { type: "message", role: "system", content: [{ type: "text", text: "You are Agent A." }] },
199
- { type: "message", role: "user", content: [{ type: "text", text: "Analyze the question." }] },
200
- ],
201
- })
202
- .llmResponses(parseNodeId("agent_b"), {
203
- model: "claude-sonnet-4-20250514",
204
- input: [
205
- { type: "message", role: "system", content: [{ type: "text", text: "You are Agent B." }] },
206
- { type: "message", role: "user", content: [{ type: "text", text: "Find edge cases." }] },
207
- ],
208
- })
209
- .joinAll(parseNodeId("join"))
210
- .llmResponses(
211
- parseNodeId("aggregate"),
212
- {
213
- model: "claude-sonnet-4-20250514",
214
- input: [
215
- { type: "message", role: "system", content: [{ type: "text", text: "Synthesize the best answer." }] },
216
- { type: "message", role: "user", content: [{ type: "text", text: "" }] },
217
- ],
218
- },
219
- {
220
- bindings: [
221
- { from: parseNodeId("join"), to: "/input/1/content/0/text", encoding: "json_string" } satisfies LLMResponsesBindingV0,
222
- ],
223
- },
224
- )
225
- .edge(parseNodeId("agent_a"), parseNodeId("join"))
226
- .edge(parseNodeId("agent_b"), parseNodeId("join"))
227
- .edge(parseNodeId("join"), parseNodeId("aggregate"))
228
- .output(parseOutputName("result"), parseNodeId("aggregate"))
229
- .build();
230
-
231
- const { run_id } = await mr.runs.create(spec);
232
-
233
- for await (const ev of await mr.runs.events(run_id)) {
234
- if (ev.type === "run_completed") {
235
- const status = await mr.runs.get(run_id);
236
- console.log("outputs:", status.outputs);
349
+ import {
350
+ ModelRelay,
351
+ APIError,
352
+ TransportError,
353
+ StreamTimeoutError,
354
+ ConfigError,
355
+ } from "@modelrelay/sdk";
356
+
357
+ try {
358
+ const response = await mr.responses.text(
359
+ "claude-sonnet-4-20250514",
360
+ "You are helpful.",
361
+ "Hello!"
362
+ );
363
+ } catch (error) {
364
+ if (error instanceof APIError) {
365
+ console.log("Status:", error.status);
366
+ console.log("Code:", error.code);
367
+ console.log("Message:", error.message);
368
+
369
+ if (error.isRateLimit()) {
370
+ // Back off and retry
371
+ } else if (error.isUnauthorized()) {
372
+ // Re-authenticate
373
+ }
374
+ } else if (error instanceof TransportError) {
375
+ console.log("Network error:", error.message);
376
+ } else if (error instanceof StreamTimeoutError) {
377
+ console.log("Stream timeout:", error.streamKind); // "ttft" | "idle" | "total"
237
378
  }
238
379
  }
239
380
  ```
240
381
 
241
- ## Token Providers (Advanced)
242
-
243
- For automatic bearer token management in data-plane calls:
244
-
245
- ### Secret key → customer bearer token
246
-
247
- ```ts
248
- import { CustomerTokenProvider, ModelRelay } from "@modelrelay/sdk";
249
-
250
- const tokenProvider = new CustomerTokenProvider({
251
- secretKey: process.env.MODELRELAY_API_KEY!,
252
- request: { projectId: "proj_...", customerId: "cust_..." },
253
- });
254
-
255
- const mr = new ModelRelay({ tokenProvider });
256
- ```
257
-
258
- ### OIDC exchange
259
-
260
- ```ts
261
- import { ModelRelay, OIDCExchangeTokenProvider, parseSecretKey } from "@modelrelay/sdk";
262
-
263
- const tokenProvider = new OIDCExchangeTokenProvider({
264
- apiKey: parseSecretKey(process.env.MODELRELAY_API_KEY!),
265
- idTokenProvider: async () => process.env.OIDC_ID_TOKEN!,
266
- });
267
-
268
- const mr = new ModelRelay({ tokenProvider });
269
- ```
270
-
271
382
  ## Configuration
272
383
 
273
384
  ```ts
@@ -278,3 +389,14 @@ const mr = new ModelRelay({
278
389
  retry: { maxAttempts: 3 },
279
390
  });
280
391
  ```
392
+
393
+ ## Documentation
394
+
395
+ For detailed guides and API reference, visit [docs.modelrelay.ai](https://docs.modelrelay.ai):
396
+
397
+ - [First Request](https://docs.modelrelay.ai/getting-started/first-request) — Make your first API call
398
+ - [Streaming](https://docs.modelrelay.ai/guides/streaming) — Real-time response streaming
399
+ - [Structured Output](https://docs.modelrelay.ai/guides/structured-output) — Get typed JSON responses
400
+ - [Tool Use](https://docs.modelrelay.ai/guides/tools) — Let models call functions
401
+ - [Error Handling](https://docs.modelrelay.ai/guides/error-handling) — Handle errors gracefully
402
+ - [Workflows](https://docs.modelrelay.ai/guides/workflows) — Multi-step AI pipelines