@modelrelay/sdk 1.3.2 → 1.3.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.
package/README.md CHANGED
@@ -1,220 +1,40 @@
1
1
  # ModelRelay TypeScript SDK
2
2
 
3
- ```bash
4
- bun add @modelrelay/sdk
5
- ```
6
-
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: { projectId: "proj_...", customerId: "cust_..." },
51
- });
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.
52
4
 
53
- const mr = new ModelRelay({ tokenProvider });
54
- ```
55
-
56
- ## Streaming Responses
57
-
58
- ```ts
59
- import { ModelRelay } from "@modelrelay/sdk";
60
-
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();
68
-
69
- const stream = await mr.responses.stream(req);
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
70
9
 
71
- for await (const event of stream) {
72
- if (event.type === "message_delta" && event.textDelta) {
73
- process.stdout.write(event.textDelta);
74
- }
75
- }
10
+ ```bash
11
+ npm install @modelrelay/sdk
12
+ # or
13
+ bun add @modelrelay/sdk
76
14
  ```
77
15
 
78
- ## Customer-Scoped Convenience
16
+ ## Quick Start
79
17
 
80
18
  ```ts
81
19
  import { ModelRelay } from "@modelrelay/sdk";
82
20
 
83
- const mr = ModelRelay.fromSecretKey("mr_sk_...");
84
- const customer = mr.forCustomer("cust_abc123");
21
+ const mr = ModelRelay.fromSecretKey(process.env.MODELRELAY_API_KEY!);
85
22
 
86
- const text = await customer.responses.text(
87
- "You are a helpful assistant.",
88
- "Summarize Q4 results",
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()
89
30
  );
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";
97
31
 
98
- const mr = ModelRelay.fromSecretKey("mr_sk_...");
99
- const customer = mr.forCustomer("cust_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: "cust_abc123",
126
- system: "You are a helpful assistant.",
127
- user: "Summarize Q4 results",
128
- });
32
+ console.log(response.text());
129
33
  ```
130
34
 
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
-
215
35
  ## Chat-Like Text Helpers
216
36
 
217
- For the most common path (**system + user → assistant text**):
37
+ For the most common path (**system + user → assistant text**), use the built-in convenience:
218
38
 
219
39
  ```ts
220
40
  const text = await mr.responses.text(
@@ -228,14 +48,16 @@ console.log(text);
228
48
  For customer-attributed requests where the backend selects the model:
229
49
 
230
50
  ```ts
231
- const text = await mr.responses.textForCustomer(
232
- "customer-123",
233
- "Answer concisely.",
234
- "Say hi.",
51
+ const customer = mr.forCustomer("customer-123");
52
+ const text = await customer.responses.text(
53
+ "You are a helpful assistant.",
54
+ "Summarize Q4 results",
235
55
  );
236
56
  ```
237
57
 
238
- To stream only message text deltas:
58
+ ## Streaming
59
+
60
+ Stream text deltas for real-time output:
239
61
 
240
62
  ```ts
241
63
  const deltas = await mr.responses.streamTextDeltas(
@@ -248,14 +70,31 @@ for await (const delta of deltas) {
248
70
  }
249
71
  ```
250
72
 
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
+
251
91
  ## Structured Outputs with Zod
252
92
 
93
+ Get typed, validated responses from the model:
94
+
253
95
  ```ts
254
- import { ModelRelay, parseSecretKey } from "@modelrelay/sdk";
255
96
  import { z } from "zod";
256
97
 
257
- const mr = new ModelRelay({ key: parseSecretKey("mr_sk_...") });
258
-
259
98
  const Person = z.object({
260
99
  name: z.string(),
261
100
  age: z.number(),
@@ -270,16 +109,11 @@ const result = await mr.responses.structured(
270
109
  console.log(result.value); // { name: "John Doe", age: 30 }
271
110
  ```
272
111
 
273
- ## Streaming Structured Outputs
112
+ ### Streaming Structured Outputs
274
113
 
275
114
  Build progressive UIs that render fields as they complete:
276
115
 
277
116
  ```ts
278
- import { ModelRelay, parseSecretKey } from "@modelrelay/sdk";
279
- import { z } from "zod";
280
-
281
- const mr = new ModelRelay({ key: parseSecretKey("mr_sk_...") });
282
-
283
117
  const Article = z.object({
284
118
  title: z.string(),
285
119
  summary: z.string(),
@@ -292,15 +126,12 @@ const stream = await mr.responses.streamStructured(
292
126
  );
293
127
 
294
128
  for await (const event of stream) {
295
- // Render fields as soon as they're complete
296
129
  if (event.completeFields.has("title")) {
297
130
  renderTitle(event.payload.title); // Safe to display
298
131
  }
299
132
  if (event.completeFields.has("summary")) {
300
133
  renderSummary(event.payload.summary);
301
134
  }
302
-
303
- // Show streaming preview of incomplete fields
304
135
  if (!event.completeFields.has("body")) {
305
136
  renderBodyPreview(event.payload.body + "▋");
306
137
  }
@@ -309,7 +140,7 @@ for await (const event of stream) {
309
140
 
310
141
  ## Customer-Attributed Requests
311
142
 
312
- For metered billing, use `customerId()` the customer's tier determines the model and `model` can be omitted:
143
+ For metered billing, use `customerId()`. The customer's tier determines the model, so `model()` can be omitted:
313
144
 
314
145
  ```ts
315
146
  const req = mr.responses
@@ -321,6 +152,16 @@ const req = mr.responses
321
152
  const stream = await mr.responses.stream(req);
322
153
  ```
323
154
 
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
+
324
165
  ## Customer Management (Backend)
325
166
 
326
167
  ```ts
@@ -341,6 +182,92 @@ const session = await mr.customers.createCheckoutSession(customer.id, {
341
182
  const status = await mr.customers.getSubscription(customer.id);
342
183
  ```
343
184
 
185
+ ## Workflow Runs
186
+
187
+ Build multi-agent workflows with parallel execution:
188
+
189
+ ```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);
237
+ }
238
+ }
239
+ ```
240
+
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
+
344
271
  ## Configuration
345
272
 
346
273
  ```ts
package/dist/index.cjs CHANGED
@@ -818,7 +818,7 @@ function isTokenReusable(token) {
818
818
  // package.json
819
819
  var package_default = {
820
820
  name: "@modelrelay/sdk",
821
- version: "1.3.2",
821
+ version: "1.3.3",
822
822
  description: "TypeScript SDK for the ModelRelay API",
823
823
  type: "module",
824
824
  main: "dist/index.cjs",
package/dist/index.js CHANGED
@@ -688,7 +688,7 @@ function isTokenReusable(token) {
688
688
  // package.json
689
689
  var package_default = {
690
690
  name: "@modelrelay/sdk",
691
- version: "1.3.2",
691
+ version: "1.3.3",
692
692
  description: "TypeScript SDK for the ModelRelay API",
693
693
  type: "module",
694
694
  main: "dist/index.cjs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelrelay/sdk",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "TypeScript SDK for the ModelRelay API",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",