@x12i/openrouter-runtime 1.0.6 → 1.1.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
@@ -100,11 +100,9 @@ Function calls are executed locally and looped back to OpenRouter until a final
100
100
 
101
101
  ## Streaming
102
102
 
103
- ### `run()` is non-streaming — always
103
+ Streaming is a **separate API**. It is never an argument on `run()`, never the default, and never available through the removed `stream()` name.
104
104
 
105
- `runtime.run(request)` never streams. It is the only supported execution path today.
106
-
107
- Use it for background tasks, graph nodes, tool calls, structured output, research jobs, classification, extraction, patch generation, and automation.
105
+ ### `run()` — non-streaming only
108
106
 
109
107
  ```ts
110
108
  const response = await runtime.run({
@@ -113,28 +111,35 @@ const response = await runtime.run({
113
111
  });
114
112
  ```
115
113
 
116
- This sends a normal non-streaming OpenRouter request and returns a completed `RuntimeResponse`.
117
-
118
- `run()` always sends `stream: false` on the OpenRouter body. Any truthy `rawOpenRouterOverrides.stream` is overwritten and reported as `STREAMING_OVERRIDE_IGNORED_FOR_RUN`. Nested `serverTools.advisor.stream` is also forced off with `ADVISOR_STREAMING_IGNORED_FOR_RUN`.
119
-
120
- ### Streaming lives on a separate method: `executeStreamingChat()`
121
-
122
- Streaming is **not** available through `run()`, and the old `runtime.stream()` name was **removed on purpose**.
114
+ - Always sends `stream: false`
115
+ - Returns a completed `RuntimeResponse`
116
+ - Use for tools, research, extraction, patches, automation
123
117
 
124
- Clients that still call `runtime.stream(...)` will break. That is intentional — so mistaken streaming usage is caught immediately.
118
+ Any truthy `rawOpenRouterOverrides.stream` is overwritten (`STREAMING_OVERRIDE_IGNORED_FOR_RUN`). Nested `advisor.stream` is forced off (`ADVISOR_STREAMING_IGNORED_FOR_RUN`).
125
119
 
126
- The reserved streaming entrypoint is:
120
+ ### `executeStreamingChat()` — streaming only
127
121
 
128
122
  ```ts
129
- for await (const event of runtime.executeStreamingChat(request)) {
130
- // token deltas / stream events (not implemented yet)
123
+ for await (const event of runtime.executeStreamingChat({
124
+ model: "openai/gpt-5.2",
125
+ prompt: "Say hello"
126
+ })) {
127
+ if (event.type === "stream.text.delta") {
128
+ process.stdout.write(event.data.text);
129
+ }
130
+ if (event.type === "stream.done") {
131
+ console.log("\nfinal:", event.data.text);
132
+ }
131
133
  }
132
134
  ```
133
135
 
134
- Today, `executeStreamingChat()` throws `STREAMING_NOT_IMPLEMENTED`. Treat this package as a non-streaming runtime until that method is implemented.
136
+ - Always sends `stream: true` (Chat Completions SSE)
137
+ - Yields typed events: `stream.start`, `stream.text.delta`, `stream.tool_call.delta`, `stream.usage`, `stream.warning`, `stream.error`, `stream.done`
138
+ - Chat Completions only — Responses / `applyPatch` must use `run()`
139
+ - Local function-tool loops are not run here; use `run()` for tool iteration
135
140
 
136
- | Method | Streaming? | Status |
141
+ | Method | Streaming? | Notes |
137
142
  | --- | --- | --- |
138
- | `runtime.run(request)` | No | Supported |
139
- | `runtime.executeStreamingChat(request)` | Yes (future) | Throws `STREAMING_NOT_IMPLEMENTED` |
143
+ | `runtime.run(request)` | No | Default execution path |
144
+ | `runtime.executeStreamingChat(request)` | Yes | Explicit streaming API |
140
145
  | `runtime.stream(request)` | — | **Removed** — breaks by design |
package/dist/index.cjs CHANGED
@@ -950,11 +950,14 @@ function compileRuntimeRequest(request, defaults, options = { apiKey: "" }, cont
950
950
  };
951
951
  }
952
952
  function enforceStreamingPolicyForRun(body, warnings, context) {
953
- if (context.streaming) return;
953
+ if (context.streaming) {
954
+ body.stream = true;
955
+ return;
956
+ }
954
957
  if (body.stream !== void 0 && body.stream !== false) {
955
958
  warnings.push({
956
959
  code: "STREAMING_OVERRIDE_IGNORED_FOR_RUN",
957
- message: "runtime.run() is non-streaming. Use runtime.executeStreamingChat() when streaming is implemented.",
960
+ message: "runtime.run() is non-streaming. Use runtime.executeStreamingChat() for streaming.",
958
961
  details: { ignoredStreamValue: body.stream }
959
962
  });
960
963
  }
@@ -1357,12 +1360,333 @@ function optional6(key, value) {
1357
1360
  return value === void 0 ? {} : { [key]: value };
1358
1361
  }
1359
1362
 
1363
+ // src/openrouter/stream-client.ts
1364
+ async function openOpenRouterStream(compiled, options, timeoutMs) {
1365
+ const controller = new AbortController();
1366
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1367
+ const cleanup = () => clearTimeout(timeout);
1368
+ try {
1369
+ const response = await options.fetch(compiled.url, {
1370
+ method: "POST",
1371
+ headers: {
1372
+ ...compiled.headers,
1373
+ Accept: "text/event-stream"
1374
+ },
1375
+ body: JSON.stringify(compiled.body),
1376
+ signal: controller.signal
1377
+ });
1378
+ if (!response.ok) {
1379
+ const text = await response.text();
1380
+ cleanup();
1381
+ const body = text ? parseBody2(text) : void 0;
1382
+ const message = extractMessage2(body) ?? `OpenRouter stream failed with status ${response.status}.`;
1383
+ throw new OpenRouterHttpError(
1384
+ response.status,
1385
+ classifyProviderFailure2(response.status, message, body),
1386
+ message,
1387
+ body,
1388
+ isRetryableStatus2(response.status)
1389
+ );
1390
+ }
1391
+ return { response, cleanup };
1392
+ } catch (error) {
1393
+ cleanup();
1394
+ if (error instanceof OpenRouterHttpError) throw error;
1395
+ if (error instanceof Error && error.name === "AbortError") {
1396
+ throw new OpenRouterHttpError(408, "PROVIDER_GATEWAY_UNREACHABLE", "OpenRouter stream timed out.", void 0, true);
1397
+ }
1398
+ throw new OpenRouterHttpError(
1399
+ 503,
1400
+ "PROVIDER_GATEWAY_UNREACHABLE",
1401
+ error instanceof Error ? error.message : "OpenRouter stream failed before receiving a response.",
1402
+ void 0,
1403
+ true
1404
+ );
1405
+ }
1406
+ }
1407
+ async function* parseSseStream(response) {
1408
+ const body = response.body;
1409
+ if (!body) {
1410
+ throw new OpenRouterHttpError(
1411
+ 503,
1412
+ "PROVIDER_GATEWAY_UNREACHABLE",
1413
+ "OpenRouter stream response had no body.",
1414
+ void 0,
1415
+ true
1416
+ );
1417
+ }
1418
+ const reader = body.getReader();
1419
+ const decoder = new TextDecoder();
1420
+ let buffer = "";
1421
+ while (true) {
1422
+ const { done, value } = await reader.read();
1423
+ if (done) break;
1424
+ buffer += decoder.decode(value, { stream: true });
1425
+ const parts = buffer.split(/\r?\n/);
1426
+ buffer = parts.pop() ?? "";
1427
+ for (const part of parts) {
1428
+ const line = part.trim();
1429
+ if (!line || line.startsWith(":")) continue;
1430
+ if (!line.startsWith("data:")) continue;
1431
+ const data = line.slice(5).trim();
1432
+ if (!data) continue;
1433
+ if (data === "[DONE]") return;
1434
+ try {
1435
+ yield JSON.parse(data);
1436
+ } catch {
1437
+ }
1438
+ }
1439
+ }
1440
+ const trailing = buffer.trim();
1441
+ if (trailing.startsWith("data:")) {
1442
+ const data = trailing.slice(5).trim();
1443
+ if (data && data !== "[DONE]") {
1444
+ try {
1445
+ yield JSON.parse(data);
1446
+ } catch {
1447
+ }
1448
+ }
1449
+ }
1450
+ }
1451
+ function parseBody2(text) {
1452
+ try {
1453
+ return JSON.parse(text);
1454
+ } catch {
1455
+ return text;
1456
+ }
1457
+ }
1458
+ function extractMessage2(body) {
1459
+ if (typeof body !== "object" || body === null) return void 0;
1460
+ const record = body;
1461
+ const error = record.error;
1462
+ if (typeof error === "object" && error !== null && typeof error.message === "string") {
1463
+ return error.message;
1464
+ }
1465
+ return typeof record.message === "string" ? record.message : void 0;
1466
+ }
1467
+ function classifyProviderFailure2(status, message, body) {
1468
+ const haystack = `${message} ${JSON.stringify(body ?? "")}`.toLowerCase();
1469
+ if (status === 401 || status === 403) return "PROVIDER_AUTH_FAILED";
1470
+ if (status === 402 || /\b(quota|credit|credits|billing|balance|insufficient funds|insufficient credit)\b/.test(haystack)) {
1471
+ return "PROVIDER_QUOTA_EXCEEDED";
1472
+ }
1473
+ if (status === 404 || /\bmodel\b.*\b(not found|not a valid|invalid|unavailable)\b/.test(haystack) || /\b(no endpoints found|not a valid model id)\b/.test(haystack)) {
1474
+ return "PROVIDER_MODEL_NOT_FOUND";
1475
+ }
1476
+ if (status === 429) return "PROVIDER_RATE_LIMITED";
1477
+ if (status === 408 || status >= 500) return "PROVIDER_GATEWAY_UNREACHABLE";
1478
+ return "PROVIDER_REQUEST_FAILED";
1479
+ }
1480
+ function isRetryableStatus2(status) {
1481
+ return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
1482
+ }
1483
+
1360
1484
  // src/runtime/streaming.ts
1361
- async function* executeStreamingChatNotImplemented() {
1362
- throw new RuntimeConfigError(
1363
- "STREAMING_NOT_IMPLEMENTED",
1364
- "executeStreamingChat() is reserved and not implemented yet. Use runtime.run() for non-streaming execution."
1365
- );
1485
+ async function* executeStreamingChat(request, context) {
1486
+ const requestId = request.id ?? "unknown";
1487
+ const options = context.options;
1488
+ const defaults = context.defaults;
1489
+ options.logger?.info?.("runtime.executeStreamingChat.started", {
1490
+ requestId,
1491
+ streaming: true,
1492
+ entrypoint: "executeStreamingChat"
1493
+ });
1494
+ try {
1495
+ assertStreamingChatRequest(request, defaults);
1496
+ const streamingRequest = {
1497
+ ...request,
1498
+ apiMode: "chat"
1499
+ };
1500
+ const compiled = compileRuntimeRequest(streamingRequest, defaults, options, { streaming: true });
1501
+ if (compiled.body.stream !== true) {
1502
+ throw new RuntimeConfigError(
1503
+ "STREAMING_CHAT_MISCONFIGURED",
1504
+ "executeStreamingChat() requires stream=true on the compiled OpenRouter body."
1505
+ );
1506
+ }
1507
+ for (const warning of compiled.warnings) {
1508
+ yield warningEvent(requestId, warning);
1509
+ }
1510
+ const model = typeof compiled.body.model === "string" ? compiled.body.model : "";
1511
+ yield startEvent(requestId, model);
1512
+ await options.hooks?.beforeOpenRouterRequest?.(compiled);
1513
+ const timeoutMs = request.execution?.timeoutMs ?? defaults.timeoutMs;
1514
+ const session = await openOpenRouterStream(compiled, options, timeoutMs);
1515
+ let text = "";
1516
+ let finishReason;
1517
+ let resolvedModel = model;
1518
+ try {
1519
+ for await (const chunk of parseSseStream(session.response)) {
1520
+ if (!isRecord(chunk)) continue;
1521
+ if (chunk.error) {
1522
+ const message = isRecord(chunk.error) && typeof chunk.error.message === "string" ? chunk.error.message : "OpenRouter stream returned an error chunk.";
1523
+ const error = {
1524
+ code: "PROVIDER_REQUEST_FAILED",
1525
+ message,
1526
+ source: "openrouter",
1527
+ details: chunk.error
1528
+ };
1529
+ yield errorEvent(requestId, error);
1530
+ throw new OpenRouterHttpError(500, "PROVIDER_REQUEST_FAILED", message, chunk, false);
1531
+ }
1532
+ if (typeof chunk.model === "string") resolvedModel = chunk.model;
1533
+ const choice = Array.isArray(chunk.choices) && isRecord(chunk.choices[0]) ? chunk.choices[0] : void 0;
1534
+ const delta = choice && isRecord(choice.delta) ? choice.delta : void 0;
1535
+ if (delta && typeof delta.content === "string" && delta.content.length > 0) {
1536
+ text += delta.content;
1537
+ yield textDeltaEvent(requestId, delta.content);
1538
+ }
1539
+ if (delta && Array.isArray(delta.tool_calls)) {
1540
+ for (const toolCall of delta.tool_calls) {
1541
+ if (!isRecord(toolCall)) continue;
1542
+ const index = typeof toolCall.index === "number" ? toolCall.index : 0;
1543
+ const fn = isRecord(toolCall.function) ? toolCall.function : {};
1544
+ yield toolCallDeltaEvent(requestId, {
1545
+ index,
1546
+ ...typeof toolCall.id === "string" ? { id: toolCall.id } : {},
1547
+ ...typeof fn.name === "string" ? { name: fn.name } : {},
1548
+ ...typeof fn.arguments === "string" ? { argumentsDelta: fn.arguments } : {}
1549
+ });
1550
+ }
1551
+ }
1552
+ if (choice && typeof choice.finish_reason === "string") {
1553
+ finishReason = choice.finish_reason;
1554
+ }
1555
+ if (chunk.usage) {
1556
+ const usage = normalizeUsage(chunk.usage);
1557
+ if (usage) yield usageEvent(requestId, usage);
1558
+ }
1559
+ }
1560
+ } finally {
1561
+ session.cleanup();
1562
+ }
1563
+ await options.hooks?.afterOpenRouterResponse?.({ streamed: true, text, finishReason, model: resolvedModel });
1564
+ yield doneEvent(requestId, {
1565
+ text,
1566
+ model: resolvedModel,
1567
+ ...finishReason ? { finishReason } : {}
1568
+ });
1569
+ options.logger?.info?.("runtime.executeStreamingChat.completed", {
1570
+ requestId,
1571
+ streaming: true,
1572
+ entrypoint: "executeStreamingChat",
1573
+ finishReason,
1574
+ textLength: text.length
1575
+ });
1576
+ } catch (error) {
1577
+ const runtimeError = toRuntimeError(error);
1578
+ yield errorEvent(requestId, runtimeError);
1579
+ options.logger?.error?.("runtime.executeStreamingChat.failed", {
1580
+ requestId,
1581
+ streaming: true,
1582
+ code: runtimeError.code,
1583
+ message: runtimeError.message
1584
+ });
1585
+ throw error instanceof Error ? error : new Error(runtimeError.message);
1586
+ }
1587
+ }
1588
+ function assertStreamingChatRequest(request, defaults) {
1589
+ if (request.apiMode === "responses") {
1590
+ throw new RuntimeConfigError(
1591
+ "STREAMING_CHAT_REQUIRES_CHAT_API",
1592
+ "executeStreamingChat() only supports Chat Completions. Use runtime.run() for Responses API requests."
1593
+ );
1594
+ }
1595
+ if (request.serverTools?.applyPatch?.mode && request.serverTools.applyPatch.mode !== "disabled") {
1596
+ throw new RuntimeConfigError(
1597
+ "STREAMING_CHAT_DOES_NOT_SUPPORT_APPLY_PATCH",
1598
+ "executeStreamingChat() does not support applyPatch. Use runtime.run() instead."
1599
+ );
1600
+ }
1601
+ if (defaults.serverTools?.applyPatch?.mode && defaults.serverTools.applyPatch.mode !== "disabled") {
1602
+ const requestPatch = request.serverTools?.applyPatch;
1603
+ if (!requestPatch || requestPatch.mode !== "disabled") {
1604
+ throw new RuntimeConfigError(
1605
+ "STREAMING_CHAT_DOES_NOT_SUPPORT_APPLY_PATCH",
1606
+ "executeStreamingChat() does not support applyPatch defaults. Use runtime.run() instead."
1607
+ );
1608
+ }
1609
+ }
1610
+ }
1611
+ function startEvent(requestId, model) {
1612
+ return {
1613
+ type: "stream.start",
1614
+ requestId,
1615
+ data: {
1616
+ model,
1617
+ apiMode: "chat",
1618
+ entrypoint: "executeStreamingChat"
1619
+ }
1620
+ };
1621
+ }
1622
+ function textDeltaEvent(requestId, text) {
1623
+ return {
1624
+ type: "stream.text.delta",
1625
+ requestId,
1626
+ data: { text }
1627
+ };
1628
+ }
1629
+ function toolCallDeltaEvent(requestId, data) {
1630
+ return {
1631
+ type: "stream.tool_call.delta",
1632
+ requestId,
1633
+ data
1634
+ };
1635
+ }
1636
+ function usageEvent(requestId, usage) {
1637
+ return {
1638
+ type: "stream.usage",
1639
+ requestId,
1640
+ data: { usage }
1641
+ };
1642
+ }
1643
+ function warningEvent(requestId, warning) {
1644
+ return {
1645
+ type: "stream.warning",
1646
+ requestId,
1647
+ data: { warning }
1648
+ };
1649
+ }
1650
+ function errorEvent(requestId, error) {
1651
+ return {
1652
+ type: "stream.error",
1653
+ requestId,
1654
+ data: { error }
1655
+ };
1656
+ }
1657
+ function doneEvent(requestId, data) {
1658
+ return {
1659
+ type: "stream.done",
1660
+ requestId,
1661
+ data
1662
+ };
1663
+ }
1664
+ function toRuntimeError(error) {
1665
+ if (error instanceof RuntimeConfigError) {
1666
+ return {
1667
+ code: error.code,
1668
+ message: error.message,
1669
+ source: "validation",
1670
+ details: error.details
1671
+ };
1672
+ }
1673
+ if (error instanceof OpenRouterHttpError) {
1674
+ return {
1675
+ code: error.code,
1676
+ message: error.message,
1677
+ source: "openrouter",
1678
+ retryable: error.retryable,
1679
+ details: {
1680
+ providerAttempts: error.providerAttempts,
1681
+ ...error.body === void 0 ? {} : { response: error.body }
1682
+ }
1683
+ };
1684
+ }
1685
+ return {
1686
+ code: "RUNTIME_ERROR",
1687
+ message: error instanceof Error ? error.message : String(error),
1688
+ source: "runtime"
1689
+ };
1366
1690
  }
1367
1691
 
1368
1692
  // src/runtime/create-runtime.ts
@@ -1403,13 +1727,8 @@ function createOpenRouterRuntime(options) {
1403
1727
  }
1404
1728
  },
1405
1729
  executeStreamingChat(request) {
1406
- resolvedOptions.logger?.warn?.("runtime.executeStreamingChat.called", {
1407
- requestId: request.id,
1408
- streaming: true,
1409
- entrypoint: "executeStreamingChat",
1410
- implemented: false
1411
- });
1412
- return executeStreamingChatNotImplemented();
1730
+ const requestWithId = { ...request, id: request.id ?? createRuntimeId() };
1731
+ return executeStreamingChat(requestWithId, context);
1413
1732
  }
1414
1733
  };
1415
1734
  function errorResponse(request, error) {