@modelrelay/sdk 0.27.0 → 1.3.0

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
@@ -4,17 +4,69 @@
4
4
  bun add @modelrelay/sdk
5
5
  ```
6
6
 
7
- ## Streaming Chat
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)
8
12
 
9
13
  ```ts
10
- import { ModelRelay } from "@modelrelay/sdk";
14
+ import { ModelRelay, OIDCExchangeTokenProvider, parseSecretKey } from "@modelrelay/sdk";
11
15
 
12
- const mr = new ModelRelay({ key: "mr_sk_..." });
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
+ ```
13
26
 
14
- const stream = await mr.chat.completions.create({
15
- model: "claude-sonnet-4-20250514",
16
- messages: [{ role: "user", content: "Hello" }],
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
+ },
17
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
+ });
52
+
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);
18
70
 
19
71
  for await (const event of stream) {
20
72
  if (event.type === "message_delta" && event.textDelta) {
@@ -23,20 +75,197 @@ for await (const event of stream) {
23
75
  }
24
76
  ```
25
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("cust_abc123");
85
+
86
+ const text = await customer.responses.text(
87
+ "You are a helpful assistant.",
88
+ "Summarize Q4 results",
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";
97
+
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.value);
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
+ });
129
+ ```
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
+
215
+ ## Chat-Like Text Helpers
216
+
217
+ For the most common path (**system + user → assistant text**):
218
+
219
+ ```ts
220
+ const text = await mr.responses.text(
221
+ "claude-sonnet-4-20250514",
222
+ "Answer concisely.",
223
+ "Say hi.",
224
+ );
225
+ console.log(text);
226
+ ```
227
+
228
+ For customer-attributed requests where the backend selects the model:
229
+
230
+ ```ts
231
+ const text = await mr.responses.textForCustomer(
232
+ "customer-123",
233
+ "Answer concisely.",
234
+ "Say hi.",
235
+ );
236
+ ```
237
+
238
+ To stream only message text deltas:
239
+
240
+ ```ts
241
+ const deltas = await mr.responses.streamTextDeltas(
242
+ "claude-sonnet-4-20250514",
243
+ "Answer concisely.",
244
+ "Say hi.",
245
+ );
246
+ for await (const delta of deltas) {
247
+ process.stdout.write(delta);
248
+ }
249
+ ```
250
+
26
251
  ## Structured Outputs with Zod
27
252
 
28
253
  ```ts
254
+ import { ModelRelay, parseSecretKey } from "@modelrelay/sdk";
29
255
  import { z } from "zod";
30
256
 
257
+ const mr = new ModelRelay({ key: parseSecretKey("mr_sk_...") });
258
+
31
259
  const Person = z.object({
32
260
  name: z.string(),
33
261
  age: z.number(),
34
262
  });
35
263
 
36
- const result = await mr.chat.completions.structured(Person, {
37
- model: "claude-sonnet-4-20250514",
38
- messages: [{ role: "user", content: "Extract: John Doe is 30" }],
39
- });
264
+ const result = await mr.responses.structured(
265
+ Person,
266
+ mr.responses.new().model("claude-sonnet-4-20250514").user("Extract: John Doe is 30").build(),
267
+ { maxRetries: 2 },
268
+ );
40
269
 
41
270
  console.log(result.value); // { name: "John Doe", age: 30 }
42
271
  ```
@@ -46,16 +275,21 @@ console.log(result.value); // { name: "John Doe", age: 30 }
46
275
  Build progressive UIs that render fields as they complete:
47
276
 
48
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
+
49
283
  const Article = z.object({
50
284
  title: z.string(),
51
285
  summary: z.string(),
52
286
  body: z.string(),
53
287
  });
54
288
 
55
- const stream = await mr.chat.completions.streamStructured(Article, {
56
- model: "claude-sonnet-4-20250514",
57
- messages: [{ role: "user", content: "Write an article about TypeScript" }],
58
- });
289
+ const stream = await mr.responses.streamStructured(
290
+ Article,
291
+ mr.responses.new().model("claude-sonnet-4-20250514").user("Write an article about TypeScript").build(),
292
+ );
59
293
 
60
294
  for await (const event of stream) {
61
295
  // Render fields as soon as they're complete
@@ -75,12 +309,16 @@ for await (const event of stream) {
75
309
 
76
310
  ## Customer-Attributed Requests
77
311
 
78
- For metered billing, use `forCustomer()` — the customer's tier determines the model:
312
+ For metered billing, use `customerId()` — the customer's tier determines the model and `model` can be omitted:
79
313
 
80
314
  ```ts
81
- const stream = await mr.chat.forCustomer("customer-123").create({
82
- messages: [{ role: "user", content: "Hello" }],
83
- });
315
+ const req = mr.responses
316
+ .new()
317
+ .customerId("customer-123")
318
+ .user("Hello")
319
+ .build();
320
+
321
+ const stream = await mr.responses.stream(req);
84
322
  ```
85
323
 
86
324
  ## Customer Management (Backend)
@@ -107,7 +345,7 @@ const status = await mr.customers.getSubscription(customer.id);
107
345
 
108
346
  ```ts
109
347
  const mr = new ModelRelay({
110
- key: "mr_sk_...",
348
+ key: parseSecretKey("mr_sk_..."),
111
349
  environment: "production", // or "staging", "sandbox"
112
350
  timeoutMs: 30_000,
113
351
  retry: { maxAttempts: 3 },