@modelrelay/sdk 0.14.1 → 0.17.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/dist/index.js CHANGED
@@ -261,7 +261,7 @@ function isTokenReusable(token) {
261
261
  // package.json
262
262
  var package_default = {
263
263
  name: "@modelrelay/sdk",
264
- version: "0.14.1",
264
+ version: "0.17.0",
265
265
  description: "TypeScript SDK for the ModelRelay API",
266
266
  type: "module",
267
267
  main: "dist/index.cjs",
@@ -320,30 +320,6 @@ var StopReasons = {
320
320
  Incomplete: "incomplete",
321
321
  Unknown: "unknown"
322
322
  };
323
- var Providers = {
324
- OpenAI: "openai",
325
- Anthropic: "anthropic",
326
- XAI: "xai",
327
- GoogleAIStudio: "google-ai-studio",
328
- Echo: "echo"
329
- };
330
- var Models = {
331
- // OpenAI models (provider-agnostic identifiers)
332
- Gpt4o: "gpt-4o",
333
- Gpt4oMini: "gpt-4o-mini",
334
- Gpt51: "gpt-5.1",
335
- // Anthropic models (provider-agnostic identifiers)
336
- Claude35HaikuLatest: "claude-3-5-haiku-latest",
337
- Claude35SonnetLatest: "claude-3-5-sonnet-latest",
338
- ClaudeOpus45: "claude-opus-4-5",
339
- Claude35Haiku: "claude-3.5-haiku",
340
- // xAI / Grok models
341
- Grok2: "grok-2",
342
- Grok4_1FastNonReasoning: "grok-4-1-fast-non-reasoning",
343
- Grok4_1FastReasoning: "grok-4-1-fast-reasoning",
344
- // Internal echo model for testing.
345
- Echo1: "echo-1"
346
- };
347
323
  function createUsage(inputTokens, outputTokens, totalTokens) {
348
324
  return {
349
325
  inputTokens,
@@ -354,14 +330,24 @@ function createUsage(inputTokens, outputTokens, totalTokens) {
354
330
  var ToolTypes = {
355
331
  Function: "function",
356
332
  Web: "web",
333
+ WebSearch: "web_search",
357
334
  XSearch: "x_search",
358
335
  CodeExecution: "code_execution"
359
336
  };
337
+ var WebToolModes = {
338
+ Search: "search",
339
+ Browse: "browse"
340
+ };
360
341
  var ToolChoiceTypes = {
361
342
  Auto: "auto",
362
343
  Required: "required",
363
344
  None: "none"
364
345
  };
346
+ var ResponseFormatTypes = {
347
+ Text: "text",
348
+ JsonObject: "json_object",
349
+ JsonSchema: "json_schema"
350
+ };
365
351
  function mergeMetrics(base, override) {
366
352
  if (!base && !override) return void 0;
367
353
  return {
@@ -395,34 +381,14 @@ function stopReasonToString(value) {
395
381
  if (typeof value === "string") return value;
396
382
  return value.other?.trim() || void 0;
397
383
  }
398
- function normalizeProvider(value) {
399
- if (value === void 0 || value === null) return void 0;
400
- const str = String(value).trim();
401
- if (!str) return void 0;
402
- const lower = str.toLowerCase();
403
- for (const p of Object.values(Providers)) {
404
- if (lower === p) return p;
405
- }
406
- return { other: str };
407
- }
408
- function providerToString(value) {
409
- if (!value) return void 0;
410
- if (typeof value === "string") return value;
411
- return value.other?.trim() || void 0;
412
- }
413
384
  function normalizeModelId(value) {
414
385
  if (value === void 0 || value === null) return void 0;
415
386
  const str = String(value).trim();
416
387
  if (!str) return void 0;
417
- const lower = str.toLowerCase();
418
- for (const m of Object.values(Models)) {
419
- if (lower === m) return m;
420
- }
421
- return { other: str };
388
+ return str;
422
389
  }
423
390
  function modelToString(value) {
424
- if (typeof value === "string") return value;
425
- return value.other?.trim() || "";
391
+ return String(value).trim();
426
392
  }
427
393
 
428
394
  // src/tools.ts
@@ -1080,7 +1046,6 @@ var ChatCompletionsClient = class {
1080
1046
  if (!hasUserMessage(params.messages)) {
1081
1047
  throw new ConfigError("at least one user message is required");
1082
1048
  }
1083
- validateRequestModel(params.model);
1084
1049
  const authHeaders = await this.auth.authForChat(params.customerId);
1085
1050
  const body = buildProxyBody(
1086
1051
  params,
@@ -1094,7 +1059,6 @@ var ChatCompletionsClient = class {
1094
1059
  const baseContext = {
1095
1060
  method: "POST",
1096
1061
  path: "/llm/proxy",
1097
- provider: params.provider,
1098
1062
  model: params.model,
1099
1063
  requestId
1100
1064
  };
@@ -1144,6 +1108,80 @@ var ChatCompletionsClient = class {
1144
1108
  trace
1145
1109
  );
1146
1110
  }
1111
+ /**
1112
+ * Stream structured JSON responses using the NDJSON contract defined for
1113
+ * /llm/proxy. The request must include a structured responseFormat.
1114
+ */
1115
+ async streamJSON(params, options = {}) {
1116
+ const metrics = mergeMetrics(this.metrics, options.metrics);
1117
+ const trace = mergeTrace(this.trace, options.trace);
1118
+ if (!params?.messages?.length) {
1119
+ throw new ConfigError("at least one message is required");
1120
+ }
1121
+ if (!hasUserMessage(params.messages)) {
1122
+ throw new ConfigError("at least one user message is required");
1123
+ }
1124
+ if (!params.responseFormat || params.responseFormat.type !== "json_object" && params.responseFormat.type !== "json_schema") {
1125
+ throw new ConfigError(
1126
+ "responseFormat with type=json_object or json_schema is required for structured streaming"
1127
+ );
1128
+ }
1129
+ const authHeaders = await this.auth.authForChat(params.customerId);
1130
+ const body = buildProxyBody(
1131
+ params,
1132
+ mergeMetadata(this.defaultMetadata, params.metadata, options.metadata)
1133
+ );
1134
+ const requestId = params.requestId || options.requestId;
1135
+ const headers = { ...options.headers || {} };
1136
+ if (requestId) {
1137
+ headers[REQUEST_ID_HEADER] = requestId;
1138
+ }
1139
+ const baseContext = {
1140
+ method: "POST",
1141
+ path: "/llm/proxy",
1142
+ model: params.model,
1143
+ requestId
1144
+ };
1145
+ const response = await this.http.request("/llm/proxy", {
1146
+ method: "POST",
1147
+ body,
1148
+ headers,
1149
+ apiKey: authHeaders.apiKey,
1150
+ accessToken: authHeaders.accessToken,
1151
+ accept: "application/x-ndjson",
1152
+ raw: true,
1153
+ signal: options.signal,
1154
+ timeoutMs: options.timeoutMs ?? 0,
1155
+ useDefaultTimeout: false,
1156
+ connectTimeoutMs: options.connectTimeoutMs,
1157
+ retry: options.retry,
1158
+ metrics,
1159
+ trace,
1160
+ context: baseContext
1161
+ });
1162
+ const resolvedRequestId = requestIdFromHeaders(response.headers) || requestId || void 0;
1163
+ if (!response.ok) {
1164
+ throw await parseErrorResponse(response);
1165
+ }
1166
+ const contentType = response.headers.get("Content-Type") || "";
1167
+ if (!/application\/(x-)?ndjson/i.test(contentType)) {
1168
+ throw new TransportError(
1169
+ `expected NDJSON structured stream, got Content-Type ${contentType || "missing"}`,
1170
+ { kind: "request" }
1171
+ );
1172
+ }
1173
+ const streamContext = {
1174
+ ...baseContext,
1175
+ requestId: resolvedRequestId ?? baseContext.requestId
1176
+ };
1177
+ return new StructuredJSONStream(
1178
+ response,
1179
+ resolvedRequestId,
1180
+ streamContext,
1181
+ metrics,
1182
+ trace
1183
+ );
1184
+ }
1147
1185
  };
1148
1186
  var ChatCompletionsStream = class {
1149
1187
  constructor(response, requestId, context, metrics, trace) {
@@ -1245,6 +1283,164 @@ var ChatCompletionsStream = class {
1245
1283
  });
1246
1284
  }
1247
1285
  };
1286
+ var StructuredJSONStream = class {
1287
+ constructor(response, requestId, context, metrics, trace) {
1288
+ this.closed = false;
1289
+ this.sawTerminal = false;
1290
+ if (!response.body) {
1291
+ throw new ConfigError("streaming response is missing a body");
1292
+ }
1293
+ this.response = response;
1294
+ this.requestId = requestId;
1295
+ this.context = context;
1296
+ this.metrics = metrics;
1297
+ this.trace = trace;
1298
+ }
1299
+ async cancel(reason) {
1300
+ this.closed = true;
1301
+ try {
1302
+ await this.response.body?.cancel(reason);
1303
+ } catch {
1304
+ }
1305
+ }
1306
+ async *[Symbol.asyncIterator]() {
1307
+ if (this.closed) {
1308
+ return;
1309
+ }
1310
+ const body = this.response.body;
1311
+ if (!body) {
1312
+ throw new ConfigError("streaming response is missing a body");
1313
+ }
1314
+ const reader = body.getReader();
1315
+ const decoder = new TextDecoder();
1316
+ let buffer = "";
1317
+ try {
1318
+ while (true) {
1319
+ if (this.closed) {
1320
+ await reader.cancel();
1321
+ return;
1322
+ }
1323
+ const { value, done } = await reader.read();
1324
+ if (done) {
1325
+ const { records: records2 } = consumeNDJSONBuffer(buffer, true);
1326
+ for (const line of records2) {
1327
+ const evt = this.parseRecord(line);
1328
+ if (evt) {
1329
+ this.traceStructuredEvent(evt, line);
1330
+ yield evt;
1331
+ }
1332
+ }
1333
+ if (!this.sawTerminal) {
1334
+ throw new TransportError(
1335
+ "structured stream ended without completion or error",
1336
+ { kind: "request" }
1337
+ );
1338
+ }
1339
+ return;
1340
+ }
1341
+ buffer += decoder.decode(value, { stream: true });
1342
+ const { records, remainder } = consumeNDJSONBuffer(buffer);
1343
+ buffer = remainder;
1344
+ for (const line of records) {
1345
+ const evt = this.parseRecord(line);
1346
+ if (evt) {
1347
+ this.traceStructuredEvent(evt, line);
1348
+ yield evt;
1349
+ }
1350
+ }
1351
+ }
1352
+ } catch (err) {
1353
+ this.trace?.streamError?.({ context: this.context, error: err });
1354
+ throw err;
1355
+ } finally {
1356
+ this.closed = true;
1357
+ reader.releaseLock();
1358
+ }
1359
+ }
1360
+ async collect() {
1361
+ let last;
1362
+ for await (const evt of this) {
1363
+ last = evt;
1364
+ if (evt.type === "completion") {
1365
+ return evt.payload;
1366
+ }
1367
+ }
1368
+ throw new TransportError(
1369
+ "structured stream ended without completion or error",
1370
+ { kind: "request" }
1371
+ );
1372
+ }
1373
+ parseRecord(line) {
1374
+ let parsed;
1375
+ try {
1376
+ parsed = JSON.parse(line);
1377
+ } catch (err) {
1378
+ throw new TransportError("invalid JSON in structured stream", {
1379
+ kind: "request",
1380
+ cause: err
1381
+ });
1382
+ }
1383
+ if (!parsed || typeof parsed !== "object") {
1384
+ throw new TransportError("structured stream record is not an object", {
1385
+ kind: "request"
1386
+ });
1387
+ }
1388
+ const obj = parsed;
1389
+ const rawType = String(obj.type || "").trim().toLowerCase();
1390
+ if (!rawType) return null;
1391
+ if (rawType === "start") {
1392
+ return null;
1393
+ }
1394
+ if (rawType === "error") {
1395
+ this.sawTerminal = true;
1396
+ const status = typeof obj.status === "number" && obj.status > 0 ? obj.status : 500;
1397
+ const message = typeof obj.message === "string" && obj.message.trim() ? obj.message : "structured stream error";
1398
+ const code = typeof obj.code === "string" && obj.code.trim() ? obj.code : void 0;
1399
+ throw new APIError(message, {
1400
+ status,
1401
+ code,
1402
+ requestId: this.requestId
1403
+ });
1404
+ }
1405
+ if (rawType !== "update" && rawType !== "completion") {
1406
+ return null;
1407
+ }
1408
+ if (obj.payload === void 0 || obj.payload === null) {
1409
+ throw new TransportError(
1410
+ "structured stream record missing payload",
1411
+ { kind: "request" }
1412
+ );
1413
+ }
1414
+ if (rawType === "completion") {
1415
+ this.sawTerminal = true;
1416
+ }
1417
+ const event = {
1418
+ type: rawType,
1419
+ // biome-ignore lint/suspicious/noExplicitAny: payload is untyped json
1420
+ payload: obj.payload,
1421
+ requestId: this.requestId
1422
+ };
1423
+ return event;
1424
+ }
1425
+ traceStructuredEvent(evt, raw) {
1426
+ if (!this.trace?.streamEvent) return;
1427
+ const event = {
1428
+ type: "custom",
1429
+ event: "structured",
1430
+ data: { type: evt.type, payload: evt.payload },
1431
+ textDelta: void 0,
1432
+ toolCallDelta: void 0,
1433
+ toolCalls: void 0,
1434
+ responseId: void 0,
1435
+ model: void 0,
1436
+ stopReason: void 0,
1437
+ usage: void 0,
1438
+ requestId: this.requestId,
1439
+ raw
1440
+ };
1441
+ this.trace.streamEvent({ context: this.context, event });
1442
+ }
1443
+ };
1248
1444
  function consumeSSEBuffer(buffer, flush = false) {
1249
1445
  const events = [];
1250
1446
  let eventName = "";
@@ -1287,6 +1483,19 @@ function consumeSSEBuffer(buffer, flush = false) {
1287
1483
  }
1288
1484
  return { events, remainder };
1289
1485
  }
1486
+ function consumeNDJSONBuffer(buffer, flush = false) {
1487
+ const lines = buffer.split(/\r?\n/);
1488
+ const records = [];
1489
+ const lastIndex = lines.length - 1;
1490
+ const limit = flush ? lines.length : Math.max(0, lastIndex);
1491
+ for (let i = 0; i < limit; i++) {
1492
+ const line = lines[i]?.trim();
1493
+ if (!line) continue;
1494
+ records.push(line);
1495
+ }
1496
+ const remainder = flush ? "" : lines[lastIndex] ?? "";
1497
+ return { records, remainder };
1498
+ }
1290
1499
  function mapChatEvent(raw, requestId) {
1291
1500
  let parsed = raw.data;
1292
1501
  if (raw.data) {
@@ -1415,7 +1624,6 @@ function normalizeChatResponse(payload, requestId) {
1415
1624
  const p = payload;
1416
1625
  const response = {
1417
1626
  id: p?.id,
1418
- provider: normalizeProvider(p?.provider),
1419
1627
  content: Array.isArray(p?.content) ? p.content : p?.content ? [String(p.content)] : [],
1420
1628
  stopReason: normalizeStopReason(p?.stop_reason),
1421
1629
  model: normalizeModelId(p?.model),
@@ -1446,19 +1654,6 @@ function normalizeUsage(payload) {
1446
1654
  const totalTokens = Number(payload.total_tokens ?? 0);
1447
1655
  return createUsage(inputTokens, outputTokens, totalTokens || void 0);
1448
1656
  }
1449
- function validateRequestModel(model) {
1450
- if (model === void 0 || model === null) return;
1451
- const value = modelToString(model).trim();
1452
- if (!value) {
1453
- throw new ConfigError("model id must be a non-empty string when provided");
1454
- }
1455
- const knownModels = Object.values(Models);
1456
- if (!knownModels.includes(value)) {
1457
- throw new ConfigError(
1458
- `unsupported model id "${value}". Use one of the SDK Models.* constants or omit model to use the tier's default model.`
1459
- );
1460
- }
1461
- }
1462
1657
  function buildProxyBody(params, metadata) {
1463
1658
  const modelValue = params.model ? modelToString(params.model).trim() : "";
1464
1659
  const body = {
@@ -1468,7 +1663,6 @@ function buildProxyBody(params, metadata) {
1468
1663
  body.model = modelValue;
1469
1664
  }
1470
1665
  if (typeof params.maxTokens === "number") body.max_tokens = params.maxTokens;
1471
- if (params.provider) body.provider = providerToString(params.provider);
1472
1666
  if (typeof params.temperature === "number")
1473
1667
  body.temperature = params.temperature;
1474
1668
  if (metadata && Object.keys(metadata).length > 0) body.metadata = metadata;
@@ -1476,6 +1670,7 @@ function buildProxyBody(params, metadata) {
1476
1670
  if (params.stopSequences?.length) body.stop_sequences = params.stopSequences;
1477
1671
  if (params.tools?.length) body.tools = normalizeTools(params.tools);
1478
1672
  if (params.toolChoice) body.tool_choice = normalizeToolChoice(params.toolChoice);
1673
+ if (params.responseFormat) body.response_format = params.responseFormat;
1479
1674
  return body;
1480
1675
  }
1481
1676
  function normalizeMessages(messages) {
@@ -1655,6 +1850,32 @@ var CustomersClient = class {
1655
1850
  });
1656
1851
  return response.customer;
1657
1852
  }
1853
+ /**
1854
+ * Claim a customer by email, setting their external_id.
1855
+ * Used when a customer subscribes via Stripe Checkout (email only) and later
1856
+ * authenticates to the app, needing to link their identity.
1857
+ *
1858
+ * @throws {APIError} with status 404 if customer not found by email
1859
+ * @throws {APIError} with status 409 if customer already claimed or external_id in use
1860
+ */
1861
+ async claim(request) {
1862
+ this.ensureSecretKey();
1863
+ if (!request.email?.trim()) {
1864
+ throw new ConfigError("email is required");
1865
+ }
1866
+ if (!isValidEmail(request.email)) {
1867
+ throw new ConfigError("invalid email format");
1868
+ }
1869
+ if (!request.external_id?.trim()) {
1870
+ throw new ConfigError("external_id is required");
1871
+ }
1872
+ const response = await this.http.json("/customers/claim", {
1873
+ method: "POST",
1874
+ body: request,
1875
+ apiKey: this.apiKey
1876
+ });
1877
+ return response.customer;
1878
+ }
1658
1879
  /**
1659
1880
  * Delete a customer by ID.
1660
1881
  */
@@ -2131,10 +2352,10 @@ export {
2131
2352
  ErrorCodes,
2132
2353
  ModelRelay,
2133
2354
  ModelRelayError,
2134
- Models,
2135
- Providers,
2355
+ ResponseFormatTypes,
2136
2356
  SDK_VERSION,
2137
2357
  StopReasons,
2358
+ StructuredJSONStream,
2138
2359
  TiersClient,
2139
2360
  ToolArgsError,
2140
2361
  ToolCallAccumulator,
@@ -2142,6 +2363,7 @@ export {
2142
2363
  ToolRegistry,
2143
2364
  ToolTypes,
2144
2365
  TransportError,
2366
+ WebToolModes,
2145
2367
  assistantMessageWithToolCalls,
2146
2368
  createAccessTokenAuth,
2147
2369
  createApiKeyAuth,
@@ -2166,12 +2388,10 @@ export {
2166
2388
  mergeTrace,
2167
2389
  modelToString,
2168
2390
  normalizeModelId,
2169
- normalizeProvider,
2170
2391
  normalizeStopReason,
2171
2392
  parseErrorResponse,
2172
2393
  parseToolArgs,
2173
2394
  parseToolArgsRaw,
2174
- providerToString,
2175
2395
  respondToToolCall,
2176
2396
  stopReasonToString,
2177
2397
  toolChoiceAuto,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelrelay/sdk",
3
- "version": "0.14.1",
3
+ "version": "0.17.0",
4
4
  "description": "TypeScript SDK for the ModelRelay API",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",