@modelrelay/sdk 0.25.1 → 0.30.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
@@ -1,434 +1,115 @@
1
1
  # ModelRelay TypeScript SDK
2
2
 
3
- Typed client for Node.js that wraps the ModelRelay API for **consuming** LLM/usage endpoints. Use secret API keys or bearer tokens issued by your backend; publishable-key frontend token flows have been removed.
4
-
5
- ## Installation
6
-
7
3
  ```bash
8
4
  bun add @modelrelay/sdk
9
- # or: npm install @modelrelay/sdk
10
5
  ```
11
6
 
12
- ## Quick Start
7
+ ## Streaming Chat
13
8
 
14
9
  ```ts
15
10
  import { ModelRelay } from "@modelrelay/sdk";
16
11
 
17
- // Use a secret key or bearer token from your backend.
18
- const mr = new ModelRelay({
19
- key: "mr_sk_..."
20
- });
21
-
22
- // Stream chat completions.
23
- const stream = await mr.chat.completions.create({
24
- model: "grok-4-1-fast-reasoning",
25
- messages: [{ role: "user", content: "Hello" }]
26
- });
27
-
28
- for await (const event of stream) {
29
- if (event.type === "message_delta" && event.textDelta) {
30
- console.log(event.textDelta);
31
- }
32
- }
33
- ```
34
-
35
- ### Server-side usage
36
-
37
- Provide a secret API key or bearer token:
38
-
39
- ```ts
40
12
  const mr = new ModelRelay({ key: "mr_sk_..." });
41
- const completion = await mr.chat.completions.create(
42
- { model: "grok-4-1-fast-reasoning", messages: [{ role: "user", content: "Hi" }], stream: false }
43
- );
44
- console.log(completion.content.join(""));
45
- ```
46
-
47
- ## Scripts (run with Bun)
48
-
49
- - `bun run build` — bundle CJS + ESM outputs with type declarations.
50
- - `bun run test` — run unit tests.
51
- - `bun run lint` — typecheck the source without emitting files.
52
-
53
- ## Configuration
54
-
55
- - **Environments**: `environment: "production" | "staging" | "sandbox"` or override `baseUrl`.
56
- - **Auth**: pass a secret/publishable `key` or a bearer `token`. Publishable keys mint frontend tokens automatically.
57
- - **Timeouts & retries**: `connectTimeoutMs` (default 5s per attempt) and `timeoutMs` (default 60s overall; set `0` to disable). Per-call overrides available on `chat.completions.create`. `retry` config (`{ maxAttempts, baseBackoffMs, maxBackoffMs, retryPost }` or `false`) controls exponential backoff with jitter.
58
- - **Headers & metadata**: `defaultHeaders` are sent with every request; `defaultMetadata` merges into every chat request and can be overridden per-call via `metadata`.
59
- - **Client header**: set `clientHeader` to override the telemetry header (defaults to `modelrelay-ts/<version>`).
60
-
61
- ### Timeouts & retry examples
62
-
63
- ```ts
64
- // Shorten connect + request timeouts globally
65
- const mr = new ModelRelay({
66
- key: "mr_sk_...",
67
- connectTimeoutMs: 3_000,
68
- timeoutMs: 20_000,
69
- retry: { maxAttempts: 4, baseBackoffMs: 200, maxBackoffMs: 2_000 }
70
- });
71
-
72
- // Per-call overrides (blocking)
73
- await mr.chat.completions.create(
74
- { model: "grok-4-1-fast-reasoning", messages: [{ role: "user", content: "Hi" }], stream: false },
75
- { timeoutMs: 5_000, retry: false }
76
- );
77
-
78
- // Streaming: keep connect timeout but disable request timeout
79
- const stream = await mr.chat.completions.create(
80
- { model: "grok-4-1-fast-reasoning", messages: [{ role: "user", content: "Hi" }] },
81
- { connectTimeoutMs: 2_000 } // request timeout is already disabled for streams by default
82
- );
83
- ```
84
13
 
85
- ### Typed models, stop reasons, and message roles
86
-
87
- - Models are plain strings (e.g., `"gpt-4o"`), so new models do not require SDK updates.
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`.
90
- - Usage backfills `totalTokens` when the backend omits it, ensuring consistent accounting.
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!" }]
14
+ const stream = await mr.chat.completions.create({
15
+ model: "claude-sonnet-4-20250514",
16
+ messages: [{ role: "user", content: "Hello" }],
113
17
  });
114
18
 
115
19
  for await (const event of stream) {
116
20
  if (event.type === "message_delta" && event.textDelta) {
117
- console.log(event.textDelta);
21
+ process.stdout.write(event.textDelta);
118
22
  }
119
23
  }
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
-
132
- ### Structured outputs (`response_format`)
133
-
134
- Request structured JSON instead of free-form text when the backend supports it:
135
-
136
- ```ts
137
- import { ModelRelay, type ResponseFormat } from "@modelrelay/sdk";
138
-
139
- const mr = new ModelRelay({ key: "mr_sk_..." });
140
-
141
- const format: ResponseFormat = {
142
- type: "json_schema",
143
- json_schema: {
144
- name: "summary",
145
- schema: {
146
- type: "object",
147
- properties: { headline: { type: "string" } },
148
- additionalProperties: false,
149
- },
150
- strict: true,
151
- },
152
- };
153
-
154
- const completion = await mr.chat.completions.create(
155
- {
156
- model: "gpt-4o-mini",
157
- messages: [{ role: "user", content: "Summarize ModelRelay" }],
158
- responseFormat: format,
159
- stream: false,
160
- },
161
- { stream: false },
162
- );
163
-
164
- console.log(completion.content[0]); // JSON string matching your schema
165
24
  ```
166
25
 
167
- ### Structured streaming (NDJSON + response_format)
168
-
169
- Use the structured streaming contract for `/llm/proxy` to stream schema-valid
170
- JSON payloads over NDJSON:
26
+ ## Structured Outputs with Zod
171
27
 
172
28
  ```ts
173
- type Item = { id: string; label: string };
174
- type RecommendationPayload = { items: Item[] };
175
-
176
- const format: ResponseFormat = {
177
- type: "json_schema",
178
- json_schema: {
179
- name: "recommendations",
180
- schema: {
181
- type: "object",
182
- properties: { items: { type: "array", items: { type: "object" } } },
183
- },
184
- },
185
- };
186
-
187
- const stream = await mr.chat.completions.streamJSON<RecommendationPayload>({
188
- model: "grok-4-1-fast",
189
- messages: [{ role: "user", content: "Recommend items for my user" }],
190
- responseFormat: format,
191
- });
192
-
193
- for await (const evt of stream) {
194
- if (evt.type === "update") {
195
- // Progressive UI: evt.payload is a partial but schema-valid payload.
196
- renderPartial(evt.payload.items);
197
- }
198
- if (evt.type === "completion") {
199
- renderFinal(evt.payload.items);
200
- }
201
- }
202
-
203
- // Prefer a single blocking result but still want structured validation?
204
- const final = await stream.collect();
205
- console.log(final.items.length);
206
- ```
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
29
  import { z } from "zod";
215
30
 
216
- const mr = new ModelRelay({ key: "mr_sk_..." });
217
-
218
- // Define your output type with Zod
219
- const PersonSchema = z.object({
31
+ const Person = z.object({
220
32
  name: z.string(),
221
33
  age: z.number(),
222
34
  });
223
35
 
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
- );
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
+ });
233
40
 
234
- console.log(`Name: ${result.value.name}, Age: ${result.value.age}`);
235
- console.log(`Succeeded on attempt ${result.attempts}`);
41
+ console.log(result.value); // { name: "John Doe", age: 30 }
236
42
  ```
237
43
 
238
- #### Schema features
44
+ ## Streaming Structured Outputs
239
45
 
240
- Zod schemas map to JSON Schema properties:
46
+ Build progressive UIs that render fields as they complete:
241
47
 
242
48
  ```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()),
49
+ const Article = z.object({
50
+ title: z.string(),
51
+ summary: z.string(),
52
+ body: z.string(),
264
53
  });
265
- ```
266
54
 
267
- #### Handling validation errors
268
-
269
- When validation fails after all retries:
270
-
271
- ```ts
272
- import { StructuredExhaustedError } from "@modelrelay/sdk";
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
+ });
273
59
 
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
- }
60
+ for await (const event of stream) {
61
+ // Render fields as soon as they're complete
62
+ if (event.completeFields.has("title")) {
63
+ renderTitle(event.payload.title); // Safe to display
293
64
  }
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" }],
65
+ if (event.completeFields.has("summary")) {
66
+ renderSummary(event.payload.summary);
335
67
  }
336
- );
337
68
 
338
- for await (const evt of stream) {
339
- if (evt.type === "completion") {
340
- console.log("Final:", evt.payload);
69
+ // Show streaming preview of incomplete fields
70
+ if (!event.completeFields.has("body")) {
71
+ renderBodyPreview(event.payload.body + "");
341
72
  }
342
73
  }
343
74
  ```
344
75
 
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
-
357
- ### Telemetry & metrics hooks
76
+ ## Customer-Attributed Requests
358
77
 
359
- Provide lightweight callbacks to observe latency and usage without extra deps:
78
+ For metered billing, use `forCustomer()` the customer's tier determines the model:
360
79
 
361
80
  ```ts
362
- const calls: string[] = [];
363
- const mr = new ModelRelay({
364
- key: "mr_sk_...",
365
- metrics: {
366
- httpRequest: (m) => calls.push(`http ${m.context.path} ${m.status} ${m.latencyMs}ms`),
367
- streamFirstToken: (m) => calls.push(`first-token ${m.latencyMs}ms`),
368
- usage: (m) => calls.push(`usage ${m.usage.totalTokens}`)
369
- },
370
- trace: {
371
- streamEvent: ({ event }) => calls.push(`event ${event.type}`),
372
- requestFinish: ({ status, latencyMs }) => calls.push(`finished ${status} in ${latencyMs}`)
373
- }
81
+ const stream = await mr.chat.forCustomer("customer-123").create({
82
+ messages: [{ role: "user", content: "Hello" }],
374
83
  });
375
-
376
- // Per-call overrides
377
- await mr.chat.completions.create(
378
- { model: "echo-1", messages: [{ role: "user", content: "hi" }] },
379
- { metrics: { usage: console.log }, trace: { streamEvent: console.debug } }
380
- );
381
84
  ```
382
85
 
383
- ### Error categories
384
-
385
- - **ConfigError**: missing key/token, invalid base URL, or request validation issues.
386
- - **TransportError**: network/connect/request/timeout failures (`kind` is one of `connect | timeout | request | other`), includes retry metadata when retries were attempted.
387
- - **APIError**: Non-2xx responses with `status`, `code`, `fields`, `requestId`, and optional `retries` metadata.
388
-
389
- ## API surface
390
-
391
- - `chat.completions.create(params, options?)`
392
- - Supports streaming (default) or blocking JSON (`stream: false`).
393
- - Accepts per-call `requestId`, `headers`, `metadata`, `timeoutMs`, and `retry` overrides.
394
- - `apiKeys.list() | create() | delete(id)` — manage API keys when using secret keys or bearer tokens.
395
- - `customers` — manage customers with a secret key (see below).
396
-
397
- ## Backend Customer Management
398
-
399
- Use a secret key (`mr_sk_*`) to manage customers from your backend:
86
+ ## Customer Management (Backend)
400
87
 
401
88
  ```ts
402
- import { ModelRelay } from "@modelrelay/sdk";
403
-
404
- const mr = new ModelRelay({ key: "mr_sk_..." });
405
-
406
- // Create or update a customer (upsert by external_id)
89
+ // Create/update customer
407
90
  const customer = await mr.customers.upsert({
408
- tier_id: "your-tier-uuid",
409
- external_id: "github-user-12345", // your app's user ID
91
+ tier_id: "tier-uuid",
92
+ external_id: "your-user-id",
410
93
  email: "user@example.com",
411
94
  });
412
95
 
413
- // List all customers
414
- const customers = await mr.customers.list();
415
-
416
- // Get a specific customer
417
- const customer = await mr.customers.get("customer-uuid");
418
-
419
- // Create a checkout session for subscription billing
420
- const session = await mr.customers.createCheckoutSession("customer-uuid", {
421
- success_url: "https://myapp.com/billing/success",
422
- cancel_url: "https://myapp.com/billing/cancel",
96
+ // Create checkout session for subscription billing
97
+ const session = await mr.customers.createCheckoutSession(customer.id, {
98
+ success_url: "https://myapp.com/success",
99
+ cancel_url: "https://myapp.com/cancel",
423
100
  });
424
- // Redirect user to session.url to complete payment
425
101
 
426
102
  // Check subscription status
427
- const status = await mr.customers.getSubscription("customer-uuid");
428
- if (status.active) {
429
- // Grant access
430
- }
103
+ const status = await mr.customers.getSubscription(customer.id);
104
+ ```
105
+
106
+ ## Configuration
431
107
 
432
- // Delete a customer
433
- await mr.customers.delete("customer-uuid");
108
+ ```ts
109
+ const mr = new ModelRelay({
110
+ key: "mr_sk_...",
111
+ environment: "production", // or "staging", "sandbox"
112
+ timeoutMs: 30_000,
113
+ retry: { maxAttempts: 3 },
114
+ });
434
115
  ```
package/dist/index.cjs CHANGED
@@ -437,7 +437,7 @@ function isTokenReusable(token) {
437
437
  // package.json
438
438
  var package_default = {
439
439
  name: "@modelrelay/sdk",
440
- version: "0.25.1",
440
+ version: "0.30.0",
441
441
  description: "TypeScript SDK for the ModelRelay API",
442
442
  type: "module",
443
443
  main: "dist/index.cjs",
@@ -527,7 +527,6 @@ var ToolChoiceTypes = {
527
527
  };
528
528
  var ResponseFormatTypes = {
529
529
  Text: "text",
530
- JsonObject: "json_object",
531
530
  JsonSchema: "json_schema"
532
531
  };
533
532
  function mergeMetrics(base, override) {
@@ -1332,7 +1331,7 @@ var ChatCompletionsClient = class {
1332
1331
  headers,
1333
1332
  apiKey: authHeaders.apiKey,
1334
1333
  accessToken: authHeaders.accessToken,
1335
- accept: stream ? "text/event-stream" : "application/json",
1334
+ accept: stream ? "application/x-ndjson" : "application/json",
1336
1335
  raw: true,
1337
1336
  signal: options.signal,
1338
1337
  timeoutMs: options.timeoutMs ?? (stream ? 0 : void 0),
@@ -1385,9 +1384,9 @@ var ChatCompletionsClient = class {
1385
1384
  if (!hasUserMessage(params.messages)) {
1386
1385
  throw new ConfigError("at least one user message is required");
1387
1386
  }
1388
- if (!params.responseFormat || params.responseFormat.type !== "json_object" && params.responseFormat.type !== "json_schema") {
1387
+ if (!params.responseFormat || params.responseFormat.type !== "json_schema") {
1389
1388
  throw new ConfigError(
1390
- "responseFormat with type=json_object or json_schema is required for structured streaming"
1389
+ "responseFormat with type=json_schema is required for structured streaming"
1391
1390
  );
1392
1391
  }
1393
1392
  const authHeaders = await this.auth.authForChat();
@@ -1644,7 +1643,7 @@ var CustomerChatClient = class {
1644
1643
  headers,
1645
1644
  apiKey: authHeaders.apiKey,
1646
1645
  accessToken: authHeaders.accessToken,
1647
- accept: stream ? "text/event-stream" : "application/json",
1646
+ accept: stream ? "application/x-ndjson" : "application/json",
1648
1647
  raw: true,
1649
1648
  signal: options.signal,
1650
1649
  timeoutMs: options.timeoutMs ?? (stream ? 0 : void 0),
@@ -1697,9 +1696,9 @@ var CustomerChatClient = class {
1697
1696
  if (!hasUserMessage(params.messages)) {
1698
1697
  throw new ConfigError("at least one user message is required");
1699
1698
  }
1700
- if (!params.responseFormat || params.responseFormat.type !== "json_object" && params.responseFormat.type !== "json_schema") {
1699
+ if (!params.responseFormat || params.responseFormat.type !== "json_schema") {
1701
1700
  throw new ConfigError(
1702
- "responseFormat with type=json_object or json_schema is required for structured streaming"
1701
+ "responseFormat with type=json_schema is required for structured streaming"
1703
1702
  );
1704
1703
  }
1705
1704
  const authHeaders = await this.auth.authForChat(this.customerId);
@@ -1926,9 +1925,9 @@ var ChatCompletionsStream = class {
1926
1925
  }
1927
1926
  const { value, done } = await reader.read();
1928
1927
  if (done) {
1929
- const { events: events2 } = consumeSSEBuffer(buffer, true);
1930
- for (const evt of events2) {
1931
- const parsed = mapChatEvent(evt, this.requestId);
1928
+ const { records: records2 } = consumeNDJSONBuffer(buffer, true);
1929
+ for (const line of records2) {
1930
+ const parsed = mapNDJSONChatEvent(line, this.requestId);
1932
1931
  if (parsed) {
1933
1932
  this.handleStreamEvent(parsed);
1934
1933
  yield parsed;
@@ -1937,10 +1936,10 @@ var ChatCompletionsStream = class {
1937
1936
  return;
1938
1937
  }
1939
1938
  buffer += decoder.decode(value, { stream: true });
1940
- const { events, remainder } = consumeSSEBuffer(buffer);
1939
+ const { records, remainder } = consumeNDJSONBuffer(buffer);
1941
1940
  buffer = remainder;
1942
- for (const evt of events) {
1943
- const parsed = mapChatEvent(evt, this.requestId);
1941
+ for (const line of records) {
1942
+ const parsed = mapNDJSONChatEvent(line, this.requestId);
1944
1943
  if (parsed) {
1945
1944
  this.handleStreamEvent(parsed);
1946
1945
  yield parsed;
@@ -2123,11 +2122,13 @@ var StructuredJSONStream = class {
2123
2122
  if (rawType === "completion") {
2124
2123
  this.sawTerminal = true;
2125
2124
  }
2125
+ const completeFieldsArray = Array.isArray(obj.complete_fields) ? obj.complete_fields.filter((f) => typeof f === "string") : [];
2126
2126
  const event = {
2127
2127
  type: rawType,
2128
2128
  // biome-ignore lint/suspicious/noExplicitAny: payload is untyped json
2129
2129
  payload: obj.payload,
2130
- requestId: this.requestId
2130
+ requestId: this.requestId,
2131
+ completeFields: new Set(completeFieldsArray)
2131
2132
  };
2132
2133
  return event;
2133
2134
  }
@@ -2150,48 +2151,6 @@ var StructuredJSONStream = class {
2150
2151
  this.trace.streamEvent({ context: this.context, event });
2151
2152
  }
2152
2153
  };
2153
- function consumeSSEBuffer(buffer, flush = false) {
2154
- const events = [];
2155
- let eventName = "";
2156
- let dataLines = [];
2157
- let remainder = "";
2158
- const lines = buffer.split(/\r?\n/);
2159
- const lastIndex = lines.length - 1;
2160
- const limit = flush ? lines.length : Math.max(0, lastIndex);
2161
- const pushEvent = () => {
2162
- if (!eventName && dataLines.length === 0) {
2163
- return;
2164
- }
2165
- events.push({
2166
- event: eventName || "message",
2167
- data: dataLines.join("\n")
2168
- });
2169
- eventName = "";
2170
- dataLines = [];
2171
- };
2172
- for (let i = 0; i < limit; i++) {
2173
- const line = lines[i];
2174
- if (line === "") {
2175
- pushEvent();
2176
- continue;
2177
- }
2178
- if (line.startsWith(":")) {
2179
- continue;
2180
- }
2181
- if (line.startsWith("event:")) {
2182
- eventName = line.slice(6).trim();
2183
- } else if (line.startsWith("data:")) {
2184
- dataLines.push(line.slice(5).trimStart());
2185
- }
2186
- }
2187
- if (flush) {
2188
- pushEvent();
2189
- remainder = "";
2190
- } else {
2191
- remainder = lines[lastIndex] ?? "";
2192
- }
2193
- return { events, remainder };
2194
- }
2195
2154
  function consumeNDJSONBuffer(buffer, flush = false) {
2196
2155
  const lines = buffer.split(/\r?\n/);
2197
2156
  const records = [];
@@ -2205,29 +2164,79 @@ function consumeNDJSONBuffer(buffer, flush = false) {
2205
2164
  const remainder = flush ? "" : lines[lastIndex] ?? "";
2206
2165
  return { records, remainder };
2207
2166
  }
2208
- function mapChatEvent(raw, requestId) {
2209
- let parsed = raw.data;
2210
- if (raw.data) {
2211
- try {
2212
- parsed = JSON.parse(raw.data);
2213
- } catch (err) {
2214
- parsed = raw.data;
2167
+ function mapNDJSONChatEvent(line, requestId) {
2168
+ let parsed;
2169
+ try {
2170
+ parsed = JSON.parse(line);
2171
+ } catch (err) {
2172
+ console.warn(
2173
+ `[ModelRelay SDK] Failed to parse NDJSON line: ${err instanceof Error ? err.message : String(err)}`,
2174
+ { line: line.substring(0, 200), requestId }
2175
+ );
2176
+ return null;
2177
+ }
2178
+ if (!parsed || typeof parsed !== "object") {
2179
+ console.warn("[ModelRelay SDK] NDJSON record is not an object", {
2180
+ parsed,
2181
+ requestId
2182
+ });
2183
+ return null;
2184
+ }
2185
+ const obj = parsed;
2186
+ const recordType = String(obj.type || "").trim().toLowerCase();
2187
+ if (recordType === "keepalive") {
2188
+ return null;
2189
+ }
2190
+ if (!recordType) {
2191
+ console.warn("[ModelRelay SDK] NDJSON record missing 'type' field", {
2192
+ obj,
2193
+ requestId
2194
+ });
2195
+ return null;
2196
+ }
2197
+ let type;
2198
+ switch (recordType) {
2199
+ case "start":
2200
+ type = "message_start";
2201
+ break;
2202
+ case "update":
2203
+ type = "message_delta";
2204
+ break;
2205
+ case "completion":
2206
+ type = "message_stop";
2207
+ break;
2208
+ case "error":
2209
+ type = "custom";
2210
+ break;
2211
+ // Tool use event types
2212
+ case "tool_use_start":
2213
+ type = "tool_use_start";
2214
+ break;
2215
+ case "tool_use_delta":
2216
+ type = "tool_use_delta";
2217
+ break;
2218
+ case "tool_use_stop":
2219
+ type = "tool_use_stop";
2220
+ break;
2221
+ default:
2222
+ type = "custom";
2223
+ }
2224
+ const usage = normalizeUsage(obj.usage);
2225
+ const responseId = obj.request_id;
2226
+ const model = normalizeModelId(obj.model);
2227
+ const stopReason = normalizeStopReason(obj.stop_reason);
2228
+ let textDelta;
2229
+ if (obj.payload && typeof obj.payload === "object") {
2230
+ if (typeof obj.payload.content === "string") {
2231
+ textDelta = obj.payload.content;
2215
2232
  }
2216
2233
  }
2217
- const payload = typeof parsed === "object" && parsed !== null ? parsed : {};
2218
- const p = payload;
2219
- const type = normalizeEventType(raw.event, p);
2220
- const usage = normalizeUsage(p.usage);
2221
- const responseId = p.response_id || p.id || p?.message?.id;
2222
- const model = normalizeModelId(p.model || p?.message?.model);
2223
- const stopReason = normalizeStopReason(p.stop_reason);
2224
- const textDelta = extractTextDelta(p);
2225
- const toolCallDelta = extractToolCallDelta(p, type);
2226
- const toolCalls = extractToolCalls(p, type);
2234
+ const toolCallDelta = extractToolCallDelta(obj, type);
2235
+ const toolCalls = extractToolCalls(obj, type);
2227
2236
  return {
2228
2237
  type,
2229
- event: raw.event || type,
2230
- data: p,
2238
+ event: recordType,
2239
+ data: obj,
2231
2240
  textDelta,
2232
2241
  toolCallDelta,
2233
2242
  toolCalls,
@@ -2236,52 +2245,9 @@ function mapChatEvent(raw, requestId) {
2236
2245
  stopReason,
2237
2246
  usage,
2238
2247
  requestId,
2239
- raw: raw.data || ""
2248
+ raw: line
2240
2249
  };
2241
2250
  }
2242
- function normalizeEventType(eventName, payload) {
2243
- const hint = String(
2244
- payload?.type || payload?.event || eventName || ""
2245
- ).trim();
2246
- switch (hint) {
2247
- case "message_start":
2248
- return "message_start";
2249
- case "message_delta":
2250
- return "message_delta";
2251
- case "message_stop":
2252
- return "message_stop";
2253
- case "tool_use_start":
2254
- return "tool_use_start";
2255
- case "tool_use_delta":
2256
- return "tool_use_delta";
2257
- case "tool_use_stop":
2258
- return "tool_use_stop";
2259
- case "ping":
2260
- return "ping";
2261
- default:
2262
- return "custom";
2263
- }
2264
- }
2265
- function extractTextDelta(payload) {
2266
- if (!payload || typeof payload !== "object") {
2267
- return void 0;
2268
- }
2269
- if (typeof payload.text_delta === "string" && payload.text_delta !== "") {
2270
- return payload.text_delta;
2271
- }
2272
- if (typeof payload.delta === "string") {
2273
- return payload.delta;
2274
- }
2275
- if (payload.delta && typeof payload.delta === "object") {
2276
- if (typeof payload.delta.text === "string") {
2277
- return payload.delta.text;
2278
- }
2279
- if (typeof payload.delta.content === "string") {
2280
- return payload.delta.content;
2281
- }
2282
- }
2283
- return void 0;
2284
- }
2285
2251
  function extractToolCallDelta(payload, type) {
2286
2252
  if (!payload || typeof payload !== "object") {
2287
2253
  return void 0;
package/dist/index.d.cts CHANGED
@@ -350,7 +350,6 @@ interface ToolCall {
350
350
  }
351
351
  declare const ResponseFormatTypes: {
352
352
  readonly Text: "text";
353
- readonly JsonObject: "json_object";
354
353
  readonly JsonSchema: "json_schema";
355
354
  };
356
355
  type ResponseFormatType = (typeof ResponseFormatTypes)[keyof typeof ResponseFormatTypes];
@@ -385,8 +384,8 @@ interface ChatCompletionCreateParams {
385
384
  */
386
385
  toolChoice?: ToolChoice;
387
386
  /**
388
- * Structured outputs configuration. When set with type `json_object` or
389
- * `json_schema`, the backend validates and returns structured JSON.
387
+ * Structured outputs configuration. When set with type `json_schema`,
388
+ * the backend validates and returns structured JSON.
390
389
  */
391
390
  responseFormat?: ResponseFormat;
392
391
  /**
@@ -418,8 +417,8 @@ interface CustomerChatParams {
418
417
  */
419
418
  toolChoice?: ToolChoice;
420
419
  /**
421
- * Structured outputs configuration. When set with type `json_object` or
422
- * `json_schema`, the backend validates and returns structured JSON.
420
+ * Structured outputs configuration. When set with type `json_schema`,
421
+ * the backend validates and returns structured JSON.
423
422
  */
424
423
  responseFormat?: ResponseFormat;
425
424
  /**
@@ -560,10 +559,29 @@ interface ChatCompletionEvent<T = unknown> {
560
559
  raw: string;
561
560
  }
562
561
  type StructuredJSONRecordType = "start" | "update" | "completion" | "error";
562
+ /**
563
+ * Recursively makes all properties optional.
564
+ * Useful for typing partial payloads during progressive streaming before
565
+ * all fields are complete.
566
+ *
567
+ * @example
568
+ * interface Article { title: string; body: string; }
569
+ * type PartialArticle = DeepPartial<Article>;
570
+ * // { title?: string; body?: string; }
571
+ */
572
+ type DeepPartial<T> = T extends object ? {
573
+ [P in keyof T]?: DeepPartial<T[P]>;
574
+ } : T;
563
575
  interface StructuredJSONEvent<T> {
564
576
  type: "update" | "completion";
565
577
  payload: T;
566
578
  requestId?: string;
579
+ /**
580
+ * Set of field paths that are complete (have their closing delimiter).
581
+ * Use dot notation for nested fields (e.g., "metadata.author").
582
+ * Check with completeFields.has("fieldName").
583
+ */
584
+ completeFields: Set<string>;
567
585
  }
568
586
  interface APIFrontendToken {
569
587
  token: string;
@@ -1884,4 +1902,4 @@ declare class ModelRelay {
1884
1902
  constructor(options: ModelRelayOptions);
1885
1903
  }
1886
1904
 
1887
- export { type APIChatResponse, type APIChatUsage, type APICheckoutSession, type APICustomerRef, APIError, type APIFrontendToken, type APIKey, type AttemptRecord, AuthClient, type AuthHeaders, ChatClient, type ChatCompletionCreateParams, type ChatCompletionEvent, type ChatCompletionResponse, ChatCompletionsStream, type ChatEventType, type ChatMessage, type CheckoutSession, type CheckoutSessionRequest, type CodeExecConfig, ConfigError, type Customer, CustomerChatClient, type CustomerChatParams, type CustomerClaimRequest, type CustomerCreateRequest, type CustomerMetadata, type CustomerUpsertRequest, CustomersClient, DEFAULT_BASE_URL, DEFAULT_CLIENT_HEADER, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS, type ErrorCategory, type ErrorCode, ErrorCodes, type FieldError, type FrontendCustomer, type FrontendToken, type FrontendTokenAutoProvisionRequest, type FrontendTokenRequest, type FunctionCall, type FunctionCallDelta, type FunctionTool, type HttpRequestMetrics, type JsonSchemaOptions, type KnownStopReason, type MessageDeltaData, type MessageRole, MessageRoles, type MessageStartData, type MessageStopData, type MetricsCallbacks, type ModelId, ModelRelay, type ModelRelayBaseOptions, ModelRelayError, type ModelRelayKeyOptions, type ModelRelayOptions, type ModelRelayOptionsLegacy, type ModelRelayTokenOptions, type NonEmptyArray, type PriceInterval, type Project, type ProviderId, type RequestContext, type ResponseFormat, type ResponseFormatType, ResponseFormatTypes, type ResponseJSONSchemaFormat, type RetryConfig, type RetryHandler, type RetryMetadata, type RetryOptions, SDK_VERSION, type Schema, type StopReason, StopReasons, type StreamFirstTokenMetrics, StructuredDecodeError, type StructuredErrorKind, StructuredExhaustedError, type StructuredJSONEvent, type StructuredJSONRecordType, StructuredJSONStream, type StructuredOptions, type StructuredResult, type SubscriptionStatus, type Tier, type TierCheckoutRequest, type TierCheckoutSession, TiersClient, type TokenType, type TokenUsageMetrics, type Tool, ToolArgsError, type ToolCall, ToolCallAccumulator, type ToolCallDelta, type ToolChoice, type ToolChoiceType, ToolChoiceTypes, type ToolExecutionResult, type ToolHandler, ToolRegistry, type ToolType, ToolTypes, type TraceCallbacks, TransportError, type TransportErrorKind, type Usage, type UsageSummary, type ValidationIssue, type WebSearchConfig, type WebToolMode, WebToolModes, type XSearchConfig, type ZodLikeSchema, assistantMessageWithToolCalls, createAccessTokenAuth, createApiKeyAuth, createAssistantMessage, createFunctionCall, createFunctionTool, createFunctionToolFromSchema, createRetryMessages, createSystemMessage, createToolCall, createUsage, createUserMessage, createWebTool, defaultRetryHandler, executeWithRetry, firstToolCall, formatToolErrorForModel, getRetryableErrors, hasRetryableErrors, hasToolCalls, isEmailRequired, isNoFreeTier, isNoTiers, isProvisioningError, isPublishableKey, mergeMetrics, mergeTrace, modelToString, normalizeModelId, normalizeStopReason, parseErrorResponse, parseToolArgs, parseToolArgsRaw, respondToToolCall, responseFormatFromZod, stopReasonToString, toolChoiceAuto, toolChoiceNone, toolChoiceRequired, toolResultMessage, tryParseToolArgs, validateWithZod, zodToJsonSchema };
1905
+ export { type APIChatResponse, type APIChatUsage, type APICheckoutSession, type APICustomerRef, APIError, type APIFrontendToken, type APIKey, type AttemptRecord, AuthClient, type AuthHeaders, ChatClient, type ChatCompletionCreateParams, type ChatCompletionEvent, type ChatCompletionResponse, ChatCompletionsStream, type ChatEventType, type ChatMessage, type CheckoutSession, type CheckoutSessionRequest, type CodeExecConfig, ConfigError, type Customer, CustomerChatClient, type CustomerChatParams, type CustomerClaimRequest, type CustomerCreateRequest, type CustomerMetadata, type CustomerUpsertRequest, CustomersClient, DEFAULT_BASE_URL, DEFAULT_CLIENT_HEADER, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS, type DeepPartial, type ErrorCategory, type ErrorCode, ErrorCodes, type FieldError, type FrontendCustomer, type FrontendToken, type FrontendTokenAutoProvisionRequest, type FrontendTokenRequest, type FunctionCall, type FunctionCallDelta, type FunctionTool, type HttpRequestMetrics, type JsonSchemaOptions, type KnownStopReason, type MessageDeltaData, type MessageRole, MessageRoles, type MessageStartData, type MessageStopData, type MetricsCallbacks, type ModelId, ModelRelay, type ModelRelayBaseOptions, ModelRelayError, type ModelRelayKeyOptions, type ModelRelayOptions, type ModelRelayOptionsLegacy, type ModelRelayTokenOptions, type NonEmptyArray, type PriceInterval, type Project, type ProviderId, type RequestContext, type ResponseFormat, type ResponseFormatType, ResponseFormatTypes, type ResponseJSONSchemaFormat, type RetryConfig, type RetryHandler, type RetryMetadata, type RetryOptions, SDK_VERSION, type Schema, type StopReason, StopReasons, type StreamFirstTokenMetrics, StructuredDecodeError, type StructuredErrorKind, StructuredExhaustedError, type StructuredJSONEvent, type StructuredJSONRecordType, StructuredJSONStream, type StructuredOptions, type StructuredResult, type SubscriptionStatus, type Tier, type TierCheckoutRequest, type TierCheckoutSession, TiersClient, type TokenType, type TokenUsageMetrics, type Tool, ToolArgsError, type ToolCall, ToolCallAccumulator, type ToolCallDelta, type ToolChoice, type ToolChoiceType, ToolChoiceTypes, type ToolExecutionResult, type ToolHandler, ToolRegistry, type ToolType, ToolTypes, type TraceCallbacks, TransportError, type TransportErrorKind, type Usage, type UsageSummary, type ValidationIssue, type WebSearchConfig, type WebToolMode, WebToolModes, type XSearchConfig, type ZodLikeSchema, assistantMessageWithToolCalls, createAccessTokenAuth, createApiKeyAuth, createAssistantMessage, createFunctionCall, createFunctionTool, createFunctionToolFromSchema, createRetryMessages, createSystemMessage, createToolCall, createUsage, createUserMessage, createWebTool, defaultRetryHandler, executeWithRetry, firstToolCall, formatToolErrorForModel, getRetryableErrors, hasRetryableErrors, hasToolCalls, isEmailRequired, isNoFreeTier, isNoTiers, isProvisioningError, isPublishableKey, mergeMetrics, mergeTrace, modelToString, normalizeModelId, normalizeStopReason, parseErrorResponse, parseToolArgs, parseToolArgsRaw, respondToToolCall, responseFormatFromZod, stopReasonToString, toolChoiceAuto, toolChoiceNone, toolChoiceRequired, toolResultMessage, tryParseToolArgs, validateWithZod, zodToJsonSchema };
package/dist/index.d.ts CHANGED
@@ -350,7 +350,6 @@ interface ToolCall {
350
350
  }
351
351
  declare const ResponseFormatTypes: {
352
352
  readonly Text: "text";
353
- readonly JsonObject: "json_object";
354
353
  readonly JsonSchema: "json_schema";
355
354
  };
356
355
  type ResponseFormatType = (typeof ResponseFormatTypes)[keyof typeof ResponseFormatTypes];
@@ -385,8 +384,8 @@ interface ChatCompletionCreateParams {
385
384
  */
386
385
  toolChoice?: ToolChoice;
387
386
  /**
388
- * Structured outputs configuration. When set with type `json_object` or
389
- * `json_schema`, the backend validates and returns structured JSON.
387
+ * Structured outputs configuration. When set with type `json_schema`,
388
+ * the backend validates and returns structured JSON.
390
389
  */
391
390
  responseFormat?: ResponseFormat;
392
391
  /**
@@ -418,8 +417,8 @@ interface CustomerChatParams {
418
417
  */
419
418
  toolChoice?: ToolChoice;
420
419
  /**
421
- * Structured outputs configuration. When set with type `json_object` or
422
- * `json_schema`, the backend validates and returns structured JSON.
420
+ * Structured outputs configuration. When set with type `json_schema`,
421
+ * the backend validates and returns structured JSON.
423
422
  */
424
423
  responseFormat?: ResponseFormat;
425
424
  /**
@@ -560,10 +559,29 @@ interface ChatCompletionEvent<T = unknown> {
560
559
  raw: string;
561
560
  }
562
561
  type StructuredJSONRecordType = "start" | "update" | "completion" | "error";
562
+ /**
563
+ * Recursively makes all properties optional.
564
+ * Useful for typing partial payloads during progressive streaming before
565
+ * all fields are complete.
566
+ *
567
+ * @example
568
+ * interface Article { title: string; body: string; }
569
+ * type PartialArticle = DeepPartial<Article>;
570
+ * // { title?: string; body?: string; }
571
+ */
572
+ type DeepPartial<T> = T extends object ? {
573
+ [P in keyof T]?: DeepPartial<T[P]>;
574
+ } : T;
563
575
  interface StructuredJSONEvent<T> {
564
576
  type: "update" | "completion";
565
577
  payload: T;
566
578
  requestId?: string;
579
+ /**
580
+ * Set of field paths that are complete (have their closing delimiter).
581
+ * Use dot notation for nested fields (e.g., "metadata.author").
582
+ * Check with completeFields.has("fieldName").
583
+ */
584
+ completeFields: Set<string>;
567
585
  }
568
586
  interface APIFrontendToken {
569
587
  token: string;
@@ -1884,4 +1902,4 @@ declare class ModelRelay {
1884
1902
  constructor(options: ModelRelayOptions);
1885
1903
  }
1886
1904
 
1887
- export { type APIChatResponse, type APIChatUsage, type APICheckoutSession, type APICustomerRef, APIError, type APIFrontendToken, type APIKey, type AttemptRecord, AuthClient, type AuthHeaders, ChatClient, type ChatCompletionCreateParams, type ChatCompletionEvent, type ChatCompletionResponse, ChatCompletionsStream, type ChatEventType, type ChatMessage, type CheckoutSession, type CheckoutSessionRequest, type CodeExecConfig, ConfigError, type Customer, CustomerChatClient, type CustomerChatParams, type CustomerClaimRequest, type CustomerCreateRequest, type CustomerMetadata, type CustomerUpsertRequest, CustomersClient, DEFAULT_BASE_URL, DEFAULT_CLIENT_HEADER, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS, type ErrorCategory, type ErrorCode, ErrorCodes, type FieldError, type FrontendCustomer, type FrontendToken, type FrontendTokenAutoProvisionRequest, type FrontendTokenRequest, type FunctionCall, type FunctionCallDelta, type FunctionTool, type HttpRequestMetrics, type JsonSchemaOptions, type KnownStopReason, type MessageDeltaData, type MessageRole, MessageRoles, type MessageStartData, type MessageStopData, type MetricsCallbacks, type ModelId, ModelRelay, type ModelRelayBaseOptions, ModelRelayError, type ModelRelayKeyOptions, type ModelRelayOptions, type ModelRelayOptionsLegacy, type ModelRelayTokenOptions, type NonEmptyArray, type PriceInterval, type Project, type ProviderId, type RequestContext, type ResponseFormat, type ResponseFormatType, ResponseFormatTypes, type ResponseJSONSchemaFormat, type RetryConfig, type RetryHandler, type RetryMetadata, type RetryOptions, SDK_VERSION, type Schema, type StopReason, StopReasons, type StreamFirstTokenMetrics, StructuredDecodeError, type StructuredErrorKind, StructuredExhaustedError, type StructuredJSONEvent, type StructuredJSONRecordType, StructuredJSONStream, type StructuredOptions, type StructuredResult, type SubscriptionStatus, type Tier, type TierCheckoutRequest, type TierCheckoutSession, TiersClient, type TokenType, type TokenUsageMetrics, type Tool, ToolArgsError, type ToolCall, ToolCallAccumulator, type ToolCallDelta, type ToolChoice, type ToolChoiceType, ToolChoiceTypes, type ToolExecutionResult, type ToolHandler, ToolRegistry, type ToolType, ToolTypes, type TraceCallbacks, TransportError, type TransportErrorKind, type Usage, type UsageSummary, type ValidationIssue, type WebSearchConfig, type WebToolMode, WebToolModes, type XSearchConfig, type ZodLikeSchema, assistantMessageWithToolCalls, createAccessTokenAuth, createApiKeyAuth, createAssistantMessage, createFunctionCall, createFunctionTool, createFunctionToolFromSchema, createRetryMessages, createSystemMessage, createToolCall, createUsage, createUserMessage, createWebTool, defaultRetryHandler, executeWithRetry, firstToolCall, formatToolErrorForModel, getRetryableErrors, hasRetryableErrors, hasToolCalls, isEmailRequired, isNoFreeTier, isNoTiers, isProvisioningError, isPublishableKey, mergeMetrics, mergeTrace, modelToString, normalizeModelId, normalizeStopReason, parseErrorResponse, parseToolArgs, parseToolArgsRaw, respondToToolCall, responseFormatFromZod, stopReasonToString, toolChoiceAuto, toolChoiceNone, toolChoiceRequired, toolResultMessage, tryParseToolArgs, validateWithZod, zodToJsonSchema };
1905
+ export { type APIChatResponse, type APIChatUsage, type APICheckoutSession, type APICustomerRef, APIError, type APIFrontendToken, type APIKey, type AttemptRecord, AuthClient, type AuthHeaders, ChatClient, type ChatCompletionCreateParams, type ChatCompletionEvent, type ChatCompletionResponse, ChatCompletionsStream, type ChatEventType, type ChatMessage, type CheckoutSession, type CheckoutSessionRequest, type CodeExecConfig, ConfigError, type Customer, CustomerChatClient, type CustomerChatParams, type CustomerClaimRequest, type CustomerCreateRequest, type CustomerMetadata, type CustomerUpsertRequest, CustomersClient, DEFAULT_BASE_URL, DEFAULT_CLIENT_HEADER, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS, type DeepPartial, type ErrorCategory, type ErrorCode, ErrorCodes, type FieldError, type FrontendCustomer, type FrontendToken, type FrontendTokenAutoProvisionRequest, type FrontendTokenRequest, type FunctionCall, type FunctionCallDelta, type FunctionTool, type HttpRequestMetrics, type JsonSchemaOptions, type KnownStopReason, type MessageDeltaData, type MessageRole, MessageRoles, type MessageStartData, type MessageStopData, type MetricsCallbacks, type ModelId, ModelRelay, type ModelRelayBaseOptions, ModelRelayError, type ModelRelayKeyOptions, type ModelRelayOptions, type ModelRelayOptionsLegacy, type ModelRelayTokenOptions, type NonEmptyArray, type PriceInterval, type Project, type ProviderId, type RequestContext, type ResponseFormat, type ResponseFormatType, ResponseFormatTypes, type ResponseJSONSchemaFormat, type RetryConfig, type RetryHandler, type RetryMetadata, type RetryOptions, SDK_VERSION, type Schema, type StopReason, StopReasons, type StreamFirstTokenMetrics, StructuredDecodeError, type StructuredErrorKind, StructuredExhaustedError, type StructuredJSONEvent, type StructuredJSONRecordType, StructuredJSONStream, type StructuredOptions, type StructuredResult, type SubscriptionStatus, type Tier, type TierCheckoutRequest, type TierCheckoutSession, TiersClient, type TokenType, type TokenUsageMetrics, type Tool, ToolArgsError, type ToolCall, ToolCallAccumulator, type ToolCallDelta, type ToolChoice, type ToolChoiceType, ToolChoiceTypes, type ToolExecutionResult, type ToolHandler, ToolRegistry, type ToolType, ToolTypes, type TraceCallbacks, TransportError, type TransportErrorKind, type Usage, type UsageSummary, type ValidationIssue, type WebSearchConfig, type WebToolMode, WebToolModes, type XSearchConfig, type ZodLikeSchema, assistantMessageWithToolCalls, createAccessTokenAuth, createApiKeyAuth, createAssistantMessage, createFunctionCall, createFunctionTool, createFunctionToolFromSchema, createRetryMessages, createSystemMessage, createToolCall, createUsage, createUserMessage, createWebTool, defaultRetryHandler, executeWithRetry, firstToolCall, formatToolErrorForModel, getRetryableErrors, hasRetryableErrors, hasToolCalls, isEmailRequired, isNoFreeTier, isNoTiers, isProvisioningError, isPublishableKey, mergeMetrics, mergeTrace, modelToString, normalizeModelId, normalizeStopReason, parseErrorResponse, parseToolArgs, parseToolArgsRaw, respondToToolCall, responseFormatFromZod, stopReasonToString, toolChoiceAuto, toolChoiceNone, toolChoiceRequired, toolResultMessage, tryParseToolArgs, validateWithZod, zodToJsonSchema };
package/dist/index.js CHANGED
@@ -340,7 +340,7 @@ function isTokenReusable(token) {
340
340
  // package.json
341
341
  var package_default = {
342
342
  name: "@modelrelay/sdk",
343
- version: "0.25.1",
343
+ version: "0.30.0",
344
344
  description: "TypeScript SDK for the ModelRelay API",
345
345
  type: "module",
346
346
  main: "dist/index.cjs",
@@ -430,7 +430,6 @@ var ToolChoiceTypes = {
430
430
  };
431
431
  var ResponseFormatTypes = {
432
432
  Text: "text",
433
- JsonObject: "json_object",
434
433
  JsonSchema: "json_schema"
435
434
  };
436
435
  function mergeMetrics(base, override) {
@@ -1235,7 +1234,7 @@ var ChatCompletionsClient = class {
1235
1234
  headers,
1236
1235
  apiKey: authHeaders.apiKey,
1237
1236
  accessToken: authHeaders.accessToken,
1238
- accept: stream ? "text/event-stream" : "application/json",
1237
+ accept: stream ? "application/x-ndjson" : "application/json",
1239
1238
  raw: true,
1240
1239
  signal: options.signal,
1241
1240
  timeoutMs: options.timeoutMs ?? (stream ? 0 : void 0),
@@ -1288,9 +1287,9 @@ var ChatCompletionsClient = class {
1288
1287
  if (!hasUserMessage(params.messages)) {
1289
1288
  throw new ConfigError("at least one user message is required");
1290
1289
  }
1291
- if (!params.responseFormat || params.responseFormat.type !== "json_object" && params.responseFormat.type !== "json_schema") {
1290
+ if (!params.responseFormat || params.responseFormat.type !== "json_schema") {
1292
1291
  throw new ConfigError(
1293
- "responseFormat with type=json_object or json_schema is required for structured streaming"
1292
+ "responseFormat with type=json_schema is required for structured streaming"
1294
1293
  );
1295
1294
  }
1296
1295
  const authHeaders = await this.auth.authForChat();
@@ -1547,7 +1546,7 @@ var CustomerChatClient = class {
1547
1546
  headers,
1548
1547
  apiKey: authHeaders.apiKey,
1549
1548
  accessToken: authHeaders.accessToken,
1550
- accept: stream ? "text/event-stream" : "application/json",
1549
+ accept: stream ? "application/x-ndjson" : "application/json",
1551
1550
  raw: true,
1552
1551
  signal: options.signal,
1553
1552
  timeoutMs: options.timeoutMs ?? (stream ? 0 : void 0),
@@ -1600,9 +1599,9 @@ var CustomerChatClient = class {
1600
1599
  if (!hasUserMessage(params.messages)) {
1601
1600
  throw new ConfigError("at least one user message is required");
1602
1601
  }
1603
- if (!params.responseFormat || params.responseFormat.type !== "json_object" && params.responseFormat.type !== "json_schema") {
1602
+ if (!params.responseFormat || params.responseFormat.type !== "json_schema") {
1604
1603
  throw new ConfigError(
1605
- "responseFormat with type=json_object or json_schema is required for structured streaming"
1604
+ "responseFormat with type=json_schema is required for structured streaming"
1606
1605
  );
1607
1606
  }
1608
1607
  const authHeaders = await this.auth.authForChat(this.customerId);
@@ -1829,9 +1828,9 @@ var ChatCompletionsStream = class {
1829
1828
  }
1830
1829
  const { value, done } = await reader.read();
1831
1830
  if (done) {
1832
- const { events: events2 } = consumeSSEBuffer(buffer, true);
1833
- for (const evt of events2) {
1834
- const parsed = mapChatEvent(evt, this.requestId);
1831
+ const { records: records2 } = consumeNDJSONBuffer(buffer, true);
1832
+ for (const line of records2) {
1833
+ const parsed = mapNDJSONChatEvent(line, this.requestId);
1835
1834
  if (parsed) {
1836
1835
  this.handleStreamEvent(parsed);
1837
1836
  yield parsed;
@@ -1840,10 +1839,10 @@ var ChatCompletionsStream = class {
1840
1839
  return;
1841
1840
  }
1842
1841
  buffer += decoder.decode(value, { stream: true });
1843
- const { events, remainder } = consumeSSEBuffer(buffer);
1842
+ const { records, remainder } = consumeNDJSONBuffer(buffer);
1844
1843
  buffer = remainder;
1845
- for (const evt of events) {
1846
- const parsed = mapChatEvent(evt, this.requestId);
1844
+ for (const line of records) {
1845
+ const parsed = mapNDJSONChatEvent(line, this.requestId);
1847
1846
  if (parsed) {
1848
1847
  this.handleStreamEvent(parsed);
1849
1848
  yield parsed;
@@ -2026,11 +2025,13 @@ var StructuredJSONStream = class {
2026
2025
  if (rawType === "completion") {
2027
2026
  this.sawTerminal = true;
2028
2027
  }
2028
+ const completeFieldsArray = Array.isArray(obj.complete_fields) ? obj.complete_fields.filter((f) => typeof f === "string") : [];
2029
2029
  const event = {
2030
2030
  type: rawType,
2031
2031
  // biome-ignore lint/suspicious/noExplicitAny: payload is untyped json
2032
2032
  payload: obj.payload,
2033
- requestId: this.requestId
2033
+ requestId: this.requestId,
2034
+ completeFields: new Set(completeFieldsArray)
2034
2035
  };
2035
2036
  return event;
2036
2037
  }
@@ -2053,48 +2054,6 @@ var StructuredJSONStream = class {
2053
2054
  this.trace.streamEvent({ context: this.context, event });
2054
2055
  }
2055
2056
  };
2056
- function consumeSSEBuffer(buffer, flush = false) {
2057
- const events = [];
2058
- let eventName = "";
2059
- let dataLines = [];
2060
- let remainder = "";
2061
- const lines = buffer.split(/\r?\n/);
2062
- const lastIndex = lines.length - 1;
2063
- const limit = flush ? lines.length : Math.max(0, lastIndex);
2064
- const pushEvent = () => {
2065
- if (!eventName && dataLines.length === 0) {
2066
- return;
2067
- }
2068
- events.push({
2069
- event: eventName || "message",
2070
- data: dataLines.join("\n")
2071
- });
2072
- eventName = "";
2073
- dataLines = [];
2074
- };
2075
- for (let i = 0; i < limit; i++) {
2076
- const line = lines[i];
2077
- if (line === "") {
2078
- pushEvent();
2079
- continue;
2080
- }
2081
- if (line.startsWith(":")) {
2082
- continue;
2083
- }
2084
- if (line.startsWith("event:")) {
2085
- eventName = line.slice(6).trim();
2086
- } else if (line.startsWith("data:")) {
2087
- dataLines.push(line.slice(5).trimStart());
2088
- }
2089
- }
2090
- if (flush) {
2091
- pushEvent();
2092
- remainder = "";
2093
- } else {
2094
- remainder = lines[lastIndex] ?? "";
2095
- }
2096
- return { events, remainder };
2097
- }
2098
2057
  function consumeNDJSONBuffer(buffer, flush = false) {
2099
2058
  const lines = buffer.split(/\r?\n/);
2100
2059
  const records = [];
@@ -2108,29 +2067,79 @@ function consumeNDJSONBuffer(buffer, flush = false) {
2108
2067
  const remainder = flush ? "" : lines[lastIndex] ?? "";
2109
2068
  return { records, remainder };
2110
2069
  }
2111
- function mapChatEvent(raw, requestId) {
2112
- let parsed = raw.data;
2113
- if (raw.data) {
2114
- try {
2115
- parsed = JSON.parse(raw.data);
2116
- } catch (err) {
2117
- parsed = raw.data;
2070
+ function mapNDJSONChatEvent(line, requestId) {
2071
+ let parsed;
2072
+ try {
2073
+ parsed = JSON.parse(line);
2074
+ } catch (err) {
2075
+ console.warn(
2076
+ `[ModelRelay SDK] Failed to parse NDJSON line: ${err instanceof Error ? err.message : String(err)}`,
2077
+ { line: line.substring(0, 200), requestId }
2078
+ );
2079
+ return null;
2080
+ }
2081
+ if (!parsed || typeof parsed !== "object") {
2082
+ console.warn("[ModelRelay SDK] NDJSON record is not an object", {
2083
+ parsed,
2084
+ requestId
2085
+ });
2086
+ return null;
2087
+ }
2088
+ const obj = parsed;
2089
+ const recordType = String(obj.type || "").trim().toLowerCase();
2090
+ if (recordType === "keepalive") {
2091
+ return null;
2092
+ }
2093
+ if (!recordType) {
2094
+ console.warn("[ModelRelay SDK] NDJSON record missing 'type' field", {
2095
+ obj,
2096
+ requestId
2097
+ });
2098
+ return null;
2099
+ }
2100
+ let type;
2101
+ switch (recordType) {
2102
+ case "start":
2103
+ type = "message_start";
2104
+ break;
2105
+ case "update":
2106
+ type = "message_delta";
2107
+ break;
2108
+ case "completion":
2109
+ type = "message_stop";
2110
+ break;
2111
+ case "error":
2112
+ type = "custom";
2113
+ break;
2114
+ // Tool use event types
2115
+ case "tool_use_start":
2116
+ type = "tool_use_start";
2117
+ break;
2118
+ case "tool_use_delta":
2119
+ type = "tool_use_delta";
2120
+ break;
2121
+ case "tool_use_stop":
2122
+ type = "tool_use_stop";
2123
+ break;
2124
+ default:
2125
+ type = "custom";
2126
+ }
2127
+ const usage = normalizeUsage(obj.usage);
2128
+ const responseId = obj.request_id;
2129
+ const model = normalizeModelId(obj.model);
2130
+ const stopReason = normalizeStopReason(obj.stop_reason);
2131
+ let textDelta;
2132
+ if (obj.payload && typeof obj.payload === "object") {
2133
+ if (typeof obj.payload.content === "string") {
2134
+ textDelta = obj.payload.content;
2118
2135
  }
2119
2136
  }
2120
- const payload = typeof parsed === "object" && parsed !== null ? parsed : {};
2121
- const p = payload;
2122
- const type = normalizeEventType(raw.event, p);
2123
- const usage = normalizeUsage(p.usage);
2124
- const responseId = p.response_id || p.id || p?.message?.id;
2125
- const model = normalizeModelId(p.model || p?.message?.model);
2126
- const stopReason = normalizeStopReason(p.stop_reason);
2127
- const textDelta = extractTextDelta(p);
2128
- const toolCallDelta = extractToolCallDelta(p, type);
2129
- const toolCalls = extractToolCalls(p, type);
2137
+ const toolCallDelta = extractToolCallDelta(obj, type);
2138
+ const toolCalls = extractToolCalls(obj, type);
2130
2139
  return {
2131
2140
  type,
2132
- event: raw.event || type,
2133
- data: p,
2141
+ event: recordType,
2142
+ data: obj,
2134
2143
  textDelta,
2135
2144
  toolCallDelta,
2136
2145
  toolCalls,
@@ -2139,52 +2148,9 @@ function mapChatEvent(raw, requestId) {
2139
2148
  stopReason,
2140
2149
  usage,
2141
2150
  requestId,
2142
- raw: raw.data || ""
2151
+ raw: line
2143
2152
  };
2144
2153
  }
2145
- function normalizeEventType(eventName, payload) {
2146
- const hint = String(
2147
- payload?.type || payload?.event || eventName || ""
2148
- ).trim();
2149
- switch (hint) {
2150
- case "message_start":
2151
- return "message_start";
2152
- case "message_delta":
2153
- return "message_delta";
2154
- case "message_stop":
2155
- return "message_stop";
2156
- case "tool_use_start":
2157
- return "tool_use_start";
2158
- case "tool_use_delta":
2159
- return "tool_use_delta";
2160
- case "tool_use_stop":
2161
- return "tool_use_stop";
2162
- case "ping":
2163
- return "ping";
2164
- default:
2165
- return "custom";
2166
- }
2167
- }
2168
- function extractTextDelta(payload) {
2169
- if (!payload || typeof payload !== "object") {
2170
- return void 0;
2171
- }
2172
- if (typeof payload.text_delta === "string" && payload.text_delta !== "") {
2173
- return payload.text_delta;
2174
- }
2175
- if (typeof payload.delta === "string") {
2176
- return payload.delta;
2177
- }
2178
- if (payload.delta && typeof payload.delta === "object") {
2179
- if (typeof payload.delta.text === "string") {
2180
- return payload.delta.text;
2181
- }
2182
- if (typeof payload.delta.content === "string") {
2183
- return payload.delta.content;
2184
- }
2185
- }
2186
- return void 0;
2187
- }
2188
2154
  function extractToolCallDelta(payload, type) {
2189
2155
  if (!payload || typeof payload !== "object") {
2190
2156
  return void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelrelay/sdk",
3
- "version": "0.25.1",
3
+ "version": "0.30.0",
4
4
  "description": "TypeScript SDK for the ModelRelay API",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",