@modelrelay/sdk 1.3.1 → 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.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
- });
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.1",
821
+ version: "1.3.3",
822
822
  description: "TypeScript SDK for the ModelRelay API",
823
823
  type: "module",
824
824
  main: "dist/index.cjs",
@@ -850,6 +850,9 @@ var package_default = {
850
850
  ],
851
851
  author: "Shane Vitarana",
852
852
  license: "Apache-2.0",
853
+ dependencies: {
854
+ "fast-json-patch": "^3.1.1"
855
+ },
853
856
  devDependencies: {
854
857
  "openapi-typescript": "^7.4.4",
855
858
  tsup: "^8.2.4",
@@ -2042,8 +2045,11 @@ function mapNDJSONResponseEvent(line, requestId) {
2042
2045
  const model = normalizeModelId(parsed.model);
2043
2046
  const stopReason = normalizeStopReason(parsed.stop_reason);
2044
2047
  let textDelta;
2045
- if (isRecord(parsed.payload) && typeof parsed.payload.content === "string") {
2046
- textDelta = parsed.payload.content;
2048
+ if (recordType2 === "update" && typeof parsed.delta === "string") {
2049
+ textDelta = parsed.delta;
2050
+ }
2051
+ if (recordType2 === "completion" && typeof parsed.content === "string") {
2052
+ textDelta = parsed.content;
2047
2053
  }
2048
2054
  const toolCallDelta = extractToolCallDelta(parsed, type);
2049
2055
  const toolCalls = extractToolCalls(parsed, type);
@@ -2143,6 +2149,7 @@ function normalizeToolCalls(toolCalls) {
2143
2149
  }
2144
2150
 
2145
2151
  // src/responses_stream.ts
2152
+ var import_fast_json_patch = require("fast-json-patch");
2146
2153
  var ResponsesStream = class {
2147
2154
  constructor(response, requestId, context, metrics, trace, timeouts = {}, startedAtMs = Date.now()) {
2148
2155
  this.firstTokenEmitted = false;
@@ -2269,6 +2276,9 @@ var ResponsesStream = class {
2269
2276
  if (evt.type === "message_stop") {
2270
2277
  stopReason = evt.stopReason;
2271
2278
  usage = evt.usage;
2279
+ if (evt.textDelta) {
2280
+ text = evt.textDelta;
2281
+ }
2272
2282
  const raw2 = isRecord(evt.data) ? evt.data : {};
2273
2283
  citations = normalizeCitations(raw2.citations) ?? citations;
2274
2284
  }
@@ -2344,6 +2354,7 @@ var StructuredJSONStream = class {
2344
2354
  this.closed = false;
2345
2355
  this.sawTerminal = false;
2346
2356
  this.firstContentSeen = false;
2357
+ this.currentPayload = {};
2347
2358
  if (!response.body) {
2348
2359
  throw new ConfigError("streaming response is missing a body");
2349
2360
  }
@@ -2487,9 +2498,36 @@ var StructuredJSONStream = class {
2487
2498
  }
2488
2499
  this.firstContentSeen = true;
2489
2500
  const completeFieldsArray = Array.isArray(parsed.complete_fields) ? parsed.complete_fields.filter((f) => typeof f === "string") : [];
2501
+ let payload;
2502
+ if (rawType === "update") {
2503
+ const patch = Array.isArray(parsed.patch) ? parsed.patch : null;
2504
+ if (!patch) {
2505
+ throw new TransportError("structured stream update missing patch", {
2506
+ kind: "request"
2507
+ });
2508
+ }
2509
+ try {
2510
+ const applied = (0, import_fast_json_patch.applyPatch)(this.currentPayload, patch, false, true);
2511
+ this.currentPayload = applied.newDocument;
2512
+ payload = this.currentPayload;
2513
+ } catch (err) {
2514
+ throw new TransportError("failed to apply structured patch", {
2515
+ kind: "request",
2516
+ cause: err
2517
+ });
2518
+ }
2519
+ } else {
2520
+ if (parsed.payload === void 0) {
2521
+ throw new TransportError("structured stream completion missing payload", {
2522
+ kind: "request"
2523
+ });
2524
+ }
2525
+ this.currentPayload = parsed.payload;
2526
+ payload = this.currentPayload;
2527
+ }
2490
2528
  return {
2491
2529
  type: rawType,
2492
- payload: parsed.payload,
2530
+ payload,
2493
2531
  requestId: this.requestId,
2494
2532
  completeFields: new Set(completeFieldsArray)
2495
2533
  };
@@ -2599,6 +2637,7 @@ async function readWithTimeout(readPromise, next, onTimeout) {
2599
2637
  }
2600
2638
 
2601
2639
  // src/responses_client.ts
2640
+ var RESPONSES_STREAM_ACCEPT = 'application/x-ndjson; profile="responses-stream/v2"';
2602
2641
  var ResponsesClient = class {
2603
2642
  constructor(http, auth, cfg = {}) {
2604
2643
  this.http = http;
@@ -2638,24 +2677,16 @@ var ResponsesClient = class {
2638
2677
  const stream = await this.stream(req, options);
2639
2678
  return {
2640
2679
  async *[Symbol.asyncIterator]() {
2641
- let accumulated = "";
2680
+ let sawDelta = false;
2642
2681
  try {
2643
2682
  for await (const evt of stream) {
2644
- if ((evt.type === "message_delta" || evt.type === "message_stop") && evt.textDelta) {
2645
- const next = evt.textDelta;
2646
- let delta = "";
2647
- if (next.startsWith(accumulated)) {
2648
- delta = next.slice(accumulated.length);
2649
- accumulated = next;
2650
- } else if (accumulated.startsWith(next)) {
2651
- accumulated = next;
2652
- } else {
2653
- delta = next;
2654
- accumulated += next;
2655
- }
2656
- if (delta) {
2657
- yield delta;
2658
- }
2683
+ if (evt.type === "message_delta" && evt.textDelta) {
2684
+ sawDelta = true;
2685
+ yield evt.textDelta;
2686
+ }
2687
+ if (evt.type === "message_stop" && evt.textDelta && !sawDelta) {
2688
+ sawDelta = true;
2689
+ yield evt.textDelta;
2659
2690
  }
2660
2691
  }
2661
2692
  } finally {
@@ -2673,24 +2704,16 @@ var ResponsesClient = class {
2673
2704
  const stream = await this.stream(req, args.options ?? options);
2674
2705
  return {
2675
2706
  async *[Symbol.asyncIterator]() {
2676
- let accumulated = "";
2707
+ let sawDelta = false;
2677
2708
  try {
2678
2709
  for await (const evt of stream) {
2679
- if ((evt.type === "message_delta" || evt.type === "message_stop") && evt.textDelta) {
2680
- const next = evt.textDelta;
2681
- let delta = "";
2682
- if (next.startsWith(accumulated)) {
2683
- delta = next.slice(accumulated.length);
2684
- accumulated = next;
2685
- } else if (accumulated.startsWith(next)) {
2686
- accumulated = next;
2687
- } else {
2688
- delta = next;
2689
- accumulated += next;
2690
- }
2691
- if (delta) {
2692
- yield delta;
2693
- }
2710
+ if (evt.type === "message_delta" && evt.textDelta) {
2711
+ sawDelta = true;
2712
+ yield evt.textDelta;
2713
+ }
2714
+ if (evt.type === "message_stop" && evt.textDelta && !sawDelta) {
2715
+ sawDelta = true;
2716
+ yield evt.textDelta;
2694
2717
  }
2695
2718
  }
2696
2719
  } finally {
@@ -2789,7 +2812,7 @@ var ResponsesClient = class {
2789
2812
  headers,
2790
2813
  apiKey: authHeaders.apiKey,
2791
2814
  accessToken: authHeaders.accessToken,
2792
- accept: "application/x-ndjson",
2815
+ accept: RESPONSES_STREAM_ACCEPT,
2793
2816
  raw: true,
2794
2817
  signal: merged.signal,
2795
2818
  timeoutMs: merged.timeoutMs ?? 0,
@@ -2869,7 +2892,7 @@ var ResponsesClient = class {
2869
2892
  headers,
2870
2893
  apiKey: authHeaders.apiKey,
2871
2894
  accessToken: authHeaders.accessToken,
2872
- accept: "application/x-ndjson",
2895
+ accept: RESPONSES_STREAM_ACCEPT,
2873
2896
  raw: true,
2874
2897
  signal: merged.signal,
2875
2898
  timeoutMs: merged.timeoutMs ?? 0,
package/dist/index.d.cts CHANGED
@@ -1668,6 +1668,7 @@ declare class StructuredJSONStream<T> implements AsyncIterable<StructuredJSONEve
1668
1668
  private closed;
1669
1669
  private sawTerminal;
1670
1670
  private firstContentSeen;
1671
+ private currentPayload;
1671
1672
  constructor(response: globalThis.Response, requestId: string | undefined, context: RequestContext, _metrics?: MetricsCallbacks, trace?: TraceCallbacks, timeouts?: StreamTimeoutsMs, startedAtMs?: number);
1672
1673
  cancel(reason?: unknown): Promise<void>;
1673
1674
  [Symbol.asyncIterator](): AsyncIterator<StructuredJSONEvent<T>>;
@@ -3405,9 +3406,25 @@ interface components {
3405
3406
  /** @description Sources from web search results */
3406
3407
  citations?: components["schemas"]["Citation"][];
3407
3408
  };
3409
+ JSONPatchOperation: {
3410
+ /** @enum {string} */
3411
+ op: "add" | "remove" | "replace" | "move" | "copy" | "test";
3412
+ path: string;
3413
+ from?: string;
3414
+ value?: {
3415
+ [key: string]: unknown;
3416
+ };
3417
+ };
3408
3418
  ResponsesStreamEnvelope: {
3409
3419
  /** @enum {string} */
3410
3420
  type: "start" | "update" | "completion" | "error" | "keepalive" | "tool_use_start" | "tool_use_delta" | "tool_use_stop";
3421
+ /** @enum {string} */
3422
+ stream_mode?: "text-delta" | "structured-patch";
3423
+ /** @enum {string} */
3424
+ stream_version?: "v2";
3425
+ delta?: string;
3426
+ content?: string;
3427
+ patch?: components["schemas"]["JSONPatchOperation"][];
3411
3428
  payload?: {
3412
3429
  [key: string]: unknown;
3413
3430
  };
@@ -3702,7 +3719,7 @@ interface components {
3702
3719
  * @description Workflow-critical model capability identifier.
3703
3720
  * @enum {string}
3704
3721
  */
3705
- ModelCapability: "tools" | "vision" | "web_search" | "computer_use" | "code_execution";
3722
+ ModelCapability: "tools" | "vision" | "web_search" | "web_fetch" | "computer_use" | "code_execution";
3706
3723
  Model: {
3707
3724
  model_id: components["schemas"]["ModelId"];
3708
3725
  provider: components["schemas"]["ProviderId"];
package/dist/index.d.ts CHANGED
@@ -1668,6 +1668,7 @@ declare class StructuredJSONStream<T> implements AsyncIterable<StructuredJSONEve
1668
1668
  private closed;
1669
1669
  private sawTerminal;
1670
1670
  private firstContentSeen;
1671
+ private currentPayload;
1671
1672
  constructor(response: globalThis.Response, requestId: string | undefined, context: RequestContext, _metrics?: MetricsCallbacks, trace?: TraceCallbacks, timeouts?: StreamTimeoutsMs, startedAtMs?: number);
1672
1673
  cancel(reason?: unknown): Promise<void>;
1673
1674
  [Symbol.asyncIterator](): AsyncIterator<StructuredJSONEvent<T>>;
@@ -3405,9 +3406,25 @@ interface components {
3405
3406
  /** @description Sources from web search results */
3406
3407
  citations?: components["schemas"]["Citation"][];
3407
3408
  };
3409
+ JSONPatchOperation: {
3410
+ /** @enum {string} */
3411
+ op: "add" | "remove" | "replace" | "move" | "copy" | "test";
3412
+ path: string;
3413
+ from?: string;
3414
+ value?: {
3415
+ [key: string]: unknown;
3416
+ };
3417
+ };
3408
3418
  ResponsesStreamEnvelope: {
3409
3419
  /** @enum {string} */
3410
3420
  type: "start" | "update" | "completion" | "error" | "keepalive" | "tool_use_start" | "tool_use_delta" | "tool_use_stop";
3421
+ /** @enum {string} */
3422
+ stream_mode?: "text-delta" | "structured-patch";
3423
+ /** @enum {string} */
3424
+ stream_version?: "v2";
3425
+ delta?: string;
3426
+ content?: string;
3427
+ patch?: components["schemas"]["JSONPatchOperation"][];
3411
3428
  payload?: {
3412
3429
  [key: string]: unknown;
3413
3430
  };
@@ -3702,7 +3719,7 @@ interface components {
3702
3719
  * @description Workflow-critical model capability identifier.
3703
3720
  * @enum {string}
3704
3721
  */
3705
- ModelCapability: "tools" | "vision" | "web_search" | "computer_use" | "code_execution";
3722
+ ModelCapability: "tools" | "vision" | "web_search" | "web_fetch" | "computer_use" | "code_execution";
3706
3723
  Model: {
3707
3724
  model_id: components["schemas"]["ModelId"];
3708
3725
  provider: components["schemas"]["ProviderId"];
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.1",
691
+ version: "1.3.3",
692
692
  description: "TypeScript SDK for the ModelRelay API",
693
693
  type: "module",
694
694
  main: "dist/index.cjs",
@@ -720,6 +720,9 @@ var package_default = {
720
720
  ],
721
721
  author: "Shane Vitarana",
722
722
  license: "Apache-2.0",
723
+ dependencies: {
724
+ "fast-json-patch": "^3.1.1"
725
+ },
723
726
  devDependencies: {
724
727
  "openapi-typescript": "^7.4.4",
725
728
  tsup: "^8.2.4",
@@ -1912,8 +1915,11 @@ function mapNDJSONResponseEvent(line, requestId) {
1912
1915
  const model = normalizeModelId(parsed.model);
1913
1916
  const stopReason = normalizeStopReason(parsed.stop_reason);
1914
1917
  let textDelta;
1915
- if (isRecord(parsed.payload) && typeof parsed.payload.content === "string") {
1916
- textDelta = parsed.payload.content;
1918
+ if (recordType2 === "update" && typeof parsed.delta === "string") {
1919
+ textDelta = parsed.delta;
1920
+ }
1921
+ if (recordType2 === "completion" && typeof parsed.content === "string") {
1922
+ textDelta = parsed.content;
1917
1923
  }
1918
1924
  const toolCallDelta = extractToolCallDelta(parsed, type);
1919
1925
  const toolCalls = extractToolCalls(parsed, type);
@@ -2013,6 +2019,7 @@ function normalizeToolCalls(toolCalls) {
2013
2019
  }
2014
2020
 
2015
2021
  // src/responses_stream.ts
2022
+ import { applyPatch } from "fast-json-patch";
2016
2023
  var ResponsesStream = class {
2017
2024
  constructor(response, requestId, context, metrics, trace, timeouts = {}, startedAtMs = Date.now()) {
2018
2025
  this.firstTokenEmitted = false;
@@ -2139,6 +2146,9 @@ var ResponsesStream = class {
2139
2146
  if (evt.type === "message_stop") {
2140
2147
  stopReason = evt.stopReason;
2141
2148
  usage = evt.usage;
2149
+ if (evt.textDelta) {
2150
+ text = evt.textDelta;
2151
+ }
2142
2152
  const raw2 = isRecord(evt.data) ? evt.data : {};
2143
2153
  citations = normalizeCitations(raw2.citations) ?? citations;
2144
2154
  }
@@ -2214,6 +2224,7 @@ var StructuredJSONStream = class {
2214
2224
  this.closed = false;
2215
2225
  this.sawTerminal = false;
2216
2226
  this.firstContentSeen = false;
2227
+ this.currentPayload = {};
2217
2228
  if (!response.body) {
2218
2229
  throw new ConfigError("streaming response is missing a body");
2219
2230
  }
@@ -2357,9 +2368,36 @@ var StructuredJSONStream = class {
2357
2368
  }
2358
2369
  this.firstContentSeen = true;
2359
2370
  const completeFieldsArray = Array.isArray(parsed.complete_fields) ? parsed.complete_fields.filter((f) => typeof f === "string") : [];
2371
+ let payload;
2372
+ if (rawType === "update") {
2373
+ const patch = Array.isArray(parsed.patch) ? parsed.patch : null;
2374
+ if (!patch) {
2375
+ throw new TransportError("structured stream update missing patch", {
2376
+ kind: "request"
2377
+ });
2378
+ }
2379
+ try {
2380
+ const applied = applyPatch(this.currentPayload, patch, false, true);
2381
+ this.currentPayload = applied.newDocument;
2382
+ payload = this.currentPayload;
2383
+ } catch (err) {
2384
+ throw new TransportError("failed to apply structured patch", {
2385
+ kind: "request",
2386
+ cause: err
2387
+ });
2388
+ }
2389
+ } else {
2390
+ if (parsed.payload === void 0) {
2391
+ throw new TransportError("structured stream completion missing payload", {
2392
+ kind: "request"
2393
+ });
2394
+ }
2395
+ this.currentPayload = parsed.payload;
2396
+ payload = this.currentPayload;
2397
+ }
2360
2398
  return {
2361
2399
  type: rawType,
2362
- payload: parsed.payload,
2400
+ payload,
2363
2401
  requestId: this.requestId,
2364
2402
  completeFields: new Set(completeFieldsArray)
2365
2403
  };
@@ -2469,6 +2507,7 @@ async function readWithTimeout(readPromise, next, onTimeout) {
2469
2507
  }
2470
2508
 
2471
2509
  // src/responses_client.ts
2510
+ var RESPONSES_STREAM_ACCEPT = 'application/x-ndjson; profile="responses-stream/v2"';
2472
2511
  var ResponsesClient = class {
2473
2512
  constructor(http, auth, cfg = {}) {
2474
2513
  this.http = http;
@@ -2508,24 +2547,16 @@ var ResponsesClient = class {
2508
2547
  const stream = await this.stream(req, options);
2509
2548
  return {
2510
2549
  async *[Symbol.asyncIterator]() {
2511
- let accumulated = "";
2550
+ let sawDelta = false;
2512
2551
  try {
2513
2552
  for await (const evt of stream) {
2514
- if ((evt.type === "message_delta" || evt.type === "message_stop") && evt.textDelta) {
2515
- const next = evt.textDelta;
2516
- let delta = "";
2517
- if (next.startsWith(accumulated)) {
2518
- delta = next.slice(accumulated.length);
2519
- accumulated = next;
2520
- } else if (accumulated.startsWith(next)) {
2521
- accumulated = next;
2522
- } else {
2523
- delta = next;
2524
- accumulated += next;
2525
- }
2526
- if (delta) {
2527
- yield delta;
2528
- }
2553
+ if (evt.type === "message_delta" && evt.textDelta) {
2554
+ sawDelta = true;
2555
+ yield evt.textDelta;
2556
+ }
2557
+ if (evt.type === "message_stop" && evt.textDelta && !sawDelta) {
2558
+ sawDelta = true;
2559
+ yield evt.textDelta;
2529
2560
  }
2530
2561
  }
2531
2562
  } finally {
@@ -2543,24 +2574,16 @@ var ResponsesClient = class {
2543
2574
  const stream = await this.stream(req, args.options ?? options);
2544
2575
  return {
2545
2576
  async *[Symbol.asyncIterator]() {
2546
- let accumulated = "";
2577
+ let sawDelta = false;
2547
2578
  try {
2548
2579
  for await (const evt of stream) {
2549
- if ((evt.type === "message_delta" || evt.type === "message_stop") && evt.textDelta) {
2550
- const next = evt.textDelta;
2551
- let delta = "";
2552
- if (next.startsWith(accumulated)) {
2553
- delta = next.slice(accumulated.length);
2554
- accumulated = next;
2555
- } else if (accumulated.startsWith(next)) {
2556
- accumulated = next;
2557
- } else {
2558
- delta = next;
2559
- accumulated += next;
2560
- }
2561
- if (delta) {
2562
- yield delta;
2563
- }
2580
+ if (evt.type === "message_delta" && evt.textDelta) {
2581
+ sawDelta = true;
2582
+ yield evt.textDelta;
2583
+ }
2584
+ if (evt.type === "message_stop" && evt.textDelta && !sawDelta) {
2585
+ sawDelta = true;
2586
+ yield evt.textDelta;
2564
2587
  }
2565
2588
  }
2566
2589
  } finally {
@@ -2659,7 +2682,7 @@ var ResponsesClient = class {
2659
2682
  headers,
2660
2683
  apiKey: authHeaders.apiKey,
2661
2684
  accessToken: authHeaders.accessToken,
2662
- accept: "application/x-ndjson",
2685
+ accept: RESPONSES_STREAM_ACCEPT,
2663
2686
  raw: true,
2664
2687
  signal: merged.signal,
2665
2688
  timeoutMs: merged.timeoutMs ?? 0,
@@ -2739,7 +2762,7 @@ var ResponsesClient = class {
2739
2762
  headers,
2740
2763
  apiKey: authHeaders.apiKey,
2741
2764
  accessToken: authHeaders.accessToken,
2742
- accept: "application/x-ndjson",
2765
+ accept: RESPONSES_STREAM_ACCEPT,
2743
2766
  raw: true,
2744
2767
  signal: merged.signal,
2745
2768
  timeoutMs: merged.timeoutMs ?? 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelrelay/sdk",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "TypeScript SDK for the ModelRelay API",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -32,6 +32,9 @@
32
32
  ],
33
33
  "author": "Shane Vitarana",
34
34
  "license": "Apache-2.0",
35
+ "dependencies": {
36
+ "fast-json-patch": "^3.1.1"
37
+ },
35
38
  "devDependencies": {
36
39
  "openapi-typescript": "^7.4.4",
37
40
  "tsup": "^8.2.4",