dsh-router-core 0.3.21 → 0.4.1

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.
Files changed (120) hide show
  1. package/README.md +238 -63
  2. package/cordis.patch.yml +22 -0
  3. package/docs/ext.md +28 -5
  4. package/docs/pool-sticky-block.md +158 -0
  5. package/docs/suppliers.md +28 -5
  6. package/lib/client-registry.js +1300 -549
  7. package/lib/client.js +1300 -549
  8. package/lib/ext-test/index.js +47 -0
  9. package/lib/ext-test/locale/en.json +6 -0
  10. package/lib/ext-test/locale/zh.json +6 -0
  11. package/lib/ext-test/package.json +5 -0
  12. package/lib/http-state-C1vb3gW2.js +48 -0
  13. package/lib/index.js +1169 -254
  14. package/lib/suppliers/{nvidia.js → nvidia/index.js} +14 -6
  15. package/lib/suppliers/nvidia/locale/en.json +6 -0
  16. package/lib/suppliers/nvidia/locale/zh.json +6 -0
  17. package/lib/suppliers/nvidia/package.json +5 -0
  18. package/lib/suppliers/{opencode.js → opencode/index.js} +14 -6
  19. package/lib/suppliers/opencode/locale/en.json +6 -0
  20. package/lib/suppliers/opencode/locale/zh.json +6 -0
  21. package/lib/suppliers/opencode/package.json +5 -0
  22. package/lib/suppliers/{openrouter.js → openrouter/index.js} +14 -6
  23. package/lib/suppliers/openrouter/locale/en.json +6 -0
  24. package/lib/suppliers/openrouter/locale/zh.json +6 -0
  25. package/lib/suppliers/openrouter/package.json +5 -0
  26. package/lib/types/ext/contract.d.ts +9 -7
  27. package/lib/types/ext-test/index.d.ts +21 -0
  28. package/lib/types/ext-test/plugin.d.ts +23 -0
  29. package/lib/types/host-version.d.ts +15 -0
  30. package/lib/types/index.d.ts +6 -0
  31. package/lib/types/llm/adapter.d.ts +35 -4
  32. package/lib/types/router/account-pool.d.ts +53 -18
  33. package/lib/types/router/index.d.ts +65 -15
  34. package/lib/types/router/prefix-affinity.d.ts +18 -1
  35. package/lib/types/router/types.d.ts +7 -4
  36. package/lib/types/router/usage-store.d.ts +12 -0
  37. package/lib/types/shared.d.ts +25 -21
  38. package/lib/types/supplier-config.d.ts +15 -3
  39. package/lib/types/suppliers/contract.d.ts +37 -4
  40. package/lib/types/suppliers/http-state.d.ts +23 -0
  41. package/lib/types/suppliers/loader.d.ts +6 -4
  42. package/lib/types/suppliers/nvidia/index.d.ts +18 -0
  43. package/lib/types/suppliers/nvidia/plugin.d.ts +13 -0
  44. package/lib/types/suppliers/opencode/index.d.ts +18 -0
  45. package/lib/types/suppliers/opencode/plugin.d.ts +7 -0
  46. package/lib/types/suppliers/openrouter/index.d.ts +18 -0
  47. package/lib/types/suppliers/openrouter/plugin.d.ts +11 -0
  48. package/lib/types/suppliers/probe.d.ts +160 -0
  49. package/lib/types/suppliers/registry.d.ts +3 -2
  50. package/package.json +50 -16
  51. package/src/client/CombosTab.tsx +8 -53
  52. package/src/client/ExtDetail.tsx +32 -196
  53. package/src/client/ExtTab.tsx +25 -42
  54. package/src/client/ExtTestPanel.tsx +392 -0
  55. package/src/client/LastHitDock.tsx +141 -0
  56. package/src/client/RouterComponentsSection.tsx +241 -0
  57. package/src/client/RouterView.tsx +12 -5
  58. package/src/client/StatsTab.tsx +8 -6
  59. package/src/client/SupplierDetail.tsx +2 -34
  60. package/src/client/comp-button-scope.test.ts +58 -0
  61. package/src/client/comp-panel-layout.test.ts +58 -0
  62. package/src/client/env.d.ts +38 -1
  63. package/src/client/ext-panels.test.ts +44 -0
  64. package/src/client/ext-panels.ts +49 -0
  65. package/src/client/index.tsx +86 -0
  66. package/src/client/last-hit-store.ts +89 -0
  67. package/src/client/model-choice.test.ts +41 -0
  68. package/src/client/model-choice.ts +37 -0
  69. package/src/client/model-hint-copy.test.ts +7 -2
  70. package/src/client/model-hint-copy.ts +2 -2
  71. package/src/client/router-components.test.ts +114 -0
  72. package/src/client/router-components.ts +104 -0
  73. package/src/client/router.css +2592 -1635
  74. package/src/client/supplier-visibility.test.ts +57 -0
  75. package/src/client/supplier-visibility.ts +63 -0
  76. package/src/ext/contract.ts +9 -7
  77. package/src/ext-test/ext-test.test.ts +60 -0
  78. package/src/ext-test/index.ts +35 -0
  79. package/src/ext-test/locale/en.json +6 -0
  80. package/src/ext-test/locale/zh.json +6 -0
  81. package/src/ext-test/plugin.ts +34 -0
  82. package/src/ext-test/row.json +5 -0
  83. package/src/host-version.test.ts +61 -0
  84. package/src/host-version.ts +101 -0
  85. package/src/index.ts +165 -68
  86. package/src/llm/adapter.test.ts +175 -4
  87. package/src/llm/adapter.ts +109 -16
  88. package/src/router/account-pool.test.ts +189 -83
  89. package/src/router/account-pool.ts +120 -31
  90. package/src/router/disconnect-leak.test.ts +7 -12
  91. package/src/router/first-byte-budget.test.ts +160 -0
  92. package/src/router/index.ts +276 -62
  93. package/src/router/models-cache.test.ts +54 -9
  94. package/src/router/precise-dispatch.test.ts +6 -6
  95. package/src/router/prefix-affinity.ts +22 -1
  96. package/src/router/supplier-switch.test.ts +220 -0
  97. package/src/router/types.ts +7 -5
  98. package/src/router/usage-store.ts +16 -0
  99. package/src/router/usage.test.ts +1 -1
  100. package/src/shared.ts +25 -24
  101. package/src/supplier-config.ts +33 -13
  102. package/src/suppliers/contract.ts +56 -4
  103. package/src/suppliers/loader.test.ts +38 -4
  104. package/src/suppliers/loader.ts +33 -18
  105. package/src/suppliers/nvidia/index.ts +27 -0
  106. package/src/suppliers/nvidia/locale/en.json +6 -0
  107. package/src/suppliers/nvidia/locale/zh.json +6 -0
  108. package/src/suppliers/nvidia/row.json +5 -0
  109. package/src/suppliers/opencode/index.ts +27 -0
  110. package/src/suppliers/opencode/locale/en.json +6 -0
  111. package/src/suppliers/opencode/locale/zh.json +6 -0
  112. package/src/suppliers/opencode/row.json +5 -0
  113. package/src/suppliers/openrouter/index.ts +27 -0
  114. package/src/suppliers/openrouter/locale/en.json +6 -0
  115. package/src/suppliers/openrouter/locale/zh.json +6 -0
  116. package/src/suppliers/openrouter/row.json +5 -0
  117. package/src/suppliers/probe.test.ts +266 -0
  118. package/src/suppliers/probe.ts +475 -0
  119. package/src/suppliers/registry.ts +103 -23
  120. package/src/types.d.ts +2 -1
@@ -8,7 +8,9 @@
8
8
  * `image` 块序列化成 OpenAI 标准的 `image_url` base64 part,经 /v1 原样透传给
9
9
  * 命中的上游供应商。最终能否看图取决于**命中的那个上游模型**——组合里是异构
10
10
  * 供应商,可能在网关端声明了图片能力、某条上游却只收文本(此时由上游自行拒收)。
11
- * 图片字节从 `ctx.attachments` 读取(读不到的按稳定占位文本降级,绝不静默丢图)。
11
+ * 图片字节从 `ctx.attachments` 读取。读不到时按稳定占位文本降级(不让整条消息失败),
12
+ * 但**必须留痕**:降级原因走 adapter 的 `log` 出口打到宿主 logger。曾经这里是
13
+ * 一个空 `catch {}`,字段改名导致的读图失败静默了跨两个内测版本(issue #8)。
12
14
  */
13
15
  import {
14
16
  EMPTY_RESPONSE_CODE,
@@ -31,15 +33,58 @@ import { mergeUsage, normalizeUsage, toTokenUsage, type UsageTokens } from '../r
31
33
  * 这一行为,不引入对 dsh-attachment 包编译期/运行期的依赖(它是宿主注入的
32
34
  * devDependency)。本地声明与原包 `AttachmentStore.readImageRequest` 形状一致,
33
35
  * 宿主注入真实 store 时天然满足。
36
+ *
37
+ * ⚠️ **第二个参数是 `ImageRequestTarget`(`width` + `height` + `maxBytes`),
38
+ * 不是 0.1.5 时代的 `ImageRequestPolicy`(`maxPixels` + `maxBytes`)**。宿主从
39
+ * 0.1.6 起在 `validateTarget()` 里校验这三个字段,缺一个就抛
40
+ * `Image request width must be a positive integer`,读图整体失败(issue #8)。
41
+ * 这两个字段不是摆设:宿主的 `pipeline()` 按 **target 的长边**把图缩到该尺寸,
42
+ * 短边由它自己推 —— 所以这里传投影后的尺寸,就是「按预算给我这张图」。
34
43
  */
35
44
  export interface RouterAttachmentStore {
36
45
  readImageRequest(
37
46
  ref: { attachmentId: string; mediaType: string; bytes: number; width: number; height: number },
38
- policy: { maxPixels: number; maxBytes: number },
47
+ target: { width: number; height: number; maxBytes: number },
39
48
  signal?: AbortSignal,
40
49
  ): Promise<{ mediaType: string; data: Uint8Array }>
41
50
  }
42
51
 
52
+ /** 送上游的请求图的像素预算(宽 × 高),沿用 0.1.5 时代 `maxPixels` 的意图。 */
53
+ const MAX_REQUEST_PIXELS = 64e4
54
+ /** 送上游的请求图的字节上限。 */
55
+ const MAX_REQUEST_BYTES = 1024 * 1024
56
+
57
+ /**
58
+ * 把源图尺寸投影到「总像素 ≤ maxPixels」的请求目标(等比、只缩不放)。
59
+ *
60
+ * 为什么需要它:宿主 0.1.6 起要的是 `ImageRequestTarget`(width/height/maxBytes),
61
+ * 而「别把一张 12MP 手机照原样塞进对话」这个意图原本靠 `maxPixels` 表达。
62
+ * 宿主自己**只按 target 的长边**缩放、短边由它推,所以我们要做的只是给出一组
63
+ * 落在预算内的正整数;短边取 floor 即可(它只参与校验与缓存键,不决定出图尺寸)。
64
+ *
65
+ * 没有直接复用官方的 `requestImageDimensions`(`@deepseek-ai/dsh-attachment` 有导出):
66
+ * 本文件刻意不依赖那个包(见 `RouterAttachmentStore` 的说明 —— 它是宿主注入的
67
+ * devDependency,引入它等于给插件加一条编译期/运行期依赖)。这 8 行是纯函数、
68
+ * 有测试钉住;真要换成官方实现时,语义等价。
69
+ */
70
+ export function projectImageDimensions(
71
+ width: number,
72
+ height: number,
73
+ maxPixels: number,
74
+ ): { width: number; height: number } {
75
+ const pixels = width * height
76
+ // 尺寸缺失/非法时不投影:让宿主自己的校验去报错,别在这里造一个假尺寸掩盖问题
77
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
78
+ return { width, height }
79
+ }
80
+ if (pixels <= maxPixels) return { width, height }
81
+ const scale = Math.sqrt(maxPixels / pixels)
82
+ return {
83
+ width: Math.max(1, Math.floor(width * scale)),
84
+ height: Math.max(1, Math.floor(height * scale)),
85
+ }
86
+ }
87
+
43
88
  /** 一张 image 块的 attachment ref 的最小形状(value 类型,够用即可)。 */
44
89
  interface RouterImgLike {
45
90
  attachmentId: string
@@ -77,6 +122,10 @@ export interface RouterAdapterSource {
77
122
  * 图片:`image` 块被序列化成 OpenAI `content` 数组里的 `image_url` part(data URI
78
123
  * base64)。图片字节经 `resolveAttachments` 读取;读不到时回退为稳定的占位文本,
79
124
  * **绝不静默丢图**(丢图 = 模型看到一条没有图的消息却毫无提示)。
125
+ *
126
+ * 工具结果双模型兼容:0.1.5/0.1.6 里 tool result 是 user 消息里的 'tool-result' 块
127
+ * (旧路径);0.1.7+ 改成了独立 role:'tool' 消息、toolCallId 挂消息级(新分支)。
128
+ * 两者在 wire 上统一序列化为 role:'tool',产物一致。见主循环里的 role==='tool' 分支。
80
129
  */
81
130
  /** tool-result 里图片挂到 user 消息时用的说明文字(与官方适配器一致)。 */
82
131
  const TOOL_RESULT_IMAGE_TEXT = 'Tool result images'
@@ -92,7 +141,7 @@ const TOOL_RESULT_IMAGE_TEXT = 'Tool result images'
92
141
  */
93
142
  const STREAM_IDLE_TIMEOUT_MS = 300_000
94
143
 
95
- async function wireMessages(options: GenerateOptions, system: string | undefined, attachments: RouterAttachmentStore | undefined): Promise<Array<Record<string, unknown>>> {
144
+ async function wireMessages(options: GenerateOptions, system: string | undefined, attachments: RouterAttachmentStore | undefined, log?: (msg: string) => void): Promise<Array<Record<string, unknown>>> {
96
145
  const out: Array<Record<string, unknown>> = []
97
146
  /**
98
147
  * 攒着「tool-result 里带出来的图片」,等这一串 tool 消息**发完**再合并成一条
@@ -139,6 +188,32 @@ async function wireMessages(options: GenerateOptions, system: string | undefined
139
188
  })
140
189
  continue
141
190
  }
191
+ // tool 结果的消息形态:**同一宿主内两种形态都合法**,按消息实际形状分派。
192
+ // 新形态:独立 role:'tool' 消息、toolCallId 挂消息级(0.1.7 loop-built);
193
+ // 旧形态:user 消息里的 'tool-result' 块(0.1.5/0.1.6,以及 0.1.7 下经
194
+ // session-format 迁移/重放的老会话历史——实测 0.1.7 仍会出现)。
195
+ // 所以**不能**用宿主版本号判断形态对错(0.1.7-alpha.2 也是 0.1.7,同样会出现
196
+ // 旧形态)——形状才是事实,按形状分派这一份 adapter 就同时吃两版。
197
+ const isToolRoleMessage = (message.role as string) === 'tool'
198
+ if (isToolRoleMessage) {
199
+ // tool 消息必须紧跟 assistant(tool_calls):中间插进任何 user 消息,上游都会判定
200
+ // tool_call 与 tool_result 失配 → codebuddy 直接 400 网关码 11148(bad_request
201
+ // 按核心设计不罚账号,面板上看不到异常,易误判成额度/风控)。
202
+ // 图片不能放进 tool 消息(实测模型会把两张不同图认成同一张),也不能插在两条 tool
203
+ // 之间(同样 400),所以先攒进 pending,等整串 tool 发完再补一条 user(图)。
204
+ const toolCallId = (message as { toolCallId?: unknown }).toolCallId
205
+ const parts = await contentParts(message.content, attachments, options.signal, log)
206
+ const imageParts = parts.filter((p) => p.type !== 'text')
207
+ const text = parts
208
+ .filter((p) => p.type === 'text')
209
+ .map((p) => (p as { text?: string }).text ?? '')
210
+ .join('')
211
+ // 空结果就发空串:不要替换成 '(no output)' 之类字面量——模型会以为工具真的打印了
212
+ // 那句话。上游接受 content:""(0.1.7 探测矩阵第 6 项确认)。
213
+ out.push({ role: 'tool', tool_call_id: String(toolCallId ?? ''), content: text })
214
+ pendingToolImages.push(...imageParts)
215
+ continue
216
+ }
142
217
  // user / tool-result
143
218
  const toolResults = message.content.filter((b) => b.type === 'tool-result')
144
219
  // 这条消息自己的(非 tool-result 的)user 部分:文本 + 图片。
@@ -146,6 +221,7 @@ async function wireMessages(options: GenerateOptions, system: string | undefined
146
221
  message.content.filter((b) => b.type !== 'tool-result'),
147
222
  attachments,
148
223
  options.signal,
224
+ log,
149
225
  )
150
226
  // 有实质 user 内容(或压根没有 tool 结果)就先发这条 user 消息;
151
227
  // 只有 tool 结果的「纯工具」消息不发空 user,图片留到 pending 里合并。
@@ -159,7 +235,7 @@ async function wireMessages(options: GenerateOptions, system: string | undefined
159
235
  // 11148「tool calls and tool results do not match」。
160
236
  // 图片不能放进 tool 消息(实测模型会把两张不同的图认成同一张),
161
237
  // 也不能插在两条 tool 之间(同样 400),所以先攒起来、发完整串再补一条 user。
162
- const parts = await contentParts(result.content, attachments, options.signal)
238
+ const parts = await contentParts(result.content, attachments, options.signal, log)
163
239
  const imageParts = parts.filter((p) => p.type !== 'text')
164
240
  const text = parts.filter((p) => p.type === 'text').map((p) => (p as { text?: string }).text ?? '').join('')
165
241
  // 空结果就发空串,不要替换成 '(no output)' 之类的字面量 —— 模型会
@@ -194,6 +270,7 @@ async function contentParts(
194
270
  blocks: ReadonlyArray<{ type: string }>,
195
271
  attachments: RouterAttachmentStore | undefined,
196
272
  signal: AbortSignal | undefined,
273
+ log?: (msg: string) => void,
197
274
  ): Promise<Array<Record<string, unknown>>> {
198
275
  const parts: Array<Record<string, unknown>> = []
199
276
  for (const block of blocks) {
@@ -204,11 +281,11 @@ async function contentParts(
204
281
  }
205
282
  if (block.type === 'image') {
206
283
  const ref = (block as unknown as { attachment: RouterImgLike }).attachment
207
- parts.push(...(await imageParts(ref, attachments, signal)))
284
+ parts.push(...(await imageParts(ref, attachments, signal, log)))
208
285
  continue
209
286
  }
210
287
  if (block.type === 'tool-result') {
211
- parts.push(...(await contentParts((block as unknown as { content: ReadonlyArray<{ type: string }> }).content, attachments, signal)))
288
+ parts.push(...(await contentParts((block as unknown as { content: ReadonlyArray<{ type: string }> }).content, attachments, signal, log)))
212
289
  }
213
290
  // 其它块(reasoning/tool-call)不属于 user content,忽略
214
291
  }
@@ -220,16 +297,23 @@ async function imageParts(
220
297
  ref: { attachmentId: string; mediaType: string; bytes: number; width: number; height: number },
221
298
  attachments: RouterAttachmentStore | undefined,
222
299
  signal: AbortSignal | undefined,
300
+ log?: (msg: string) => void,
223
301
  ): Promise<Array<Record<string, unknown>>> {
224
- if (attachments === undefined) return [{ type: 'text', text: imagePlaceholder(ref.attachmentId) }]
302
+ if (attachments === undefined) {
303
+ log?.('图片附件 store 不可用(ctx.attachments 未注入),该图降级为占位文本')
304
+ return [{ type: 'text', text: imagePlaceholder(ref.attachmentId) }]
305
+ }
306
+ const ref0 = { attachmentId: ref.attachmentId, mediaType: ref.mediaType, bytes: ref.bytes, width: ref.width, height: ref.height }
307
+ // 目标 = 源尺寸按 64e4 像素预算等比投影后的宽高 + 字节上限(0.1.6+ 的 target 形状)
308
+ const target = { ...projectImageDimensions(ref.width, ref.height, MAX_REQUEST_PIXELS), maxBytes: MAX_REQUEST_BYTES }
225
309
  try {
226
- const img = await attachments.readImageRequest(
227
- { attachmentId: ref.attachmentId, mediaType: ref.mediaType, bytes: ref.bytes, width: ref.width, height: ref.height },
228
- { maxPixels: 64e4, maxBytes: 1024 * 1024 },
229
- signal,
230
- )
310
+ const img = await attachments.readImageRequest(ref0, target, signal)
231
311
  return [{ type: 'image_url', image_url: { url: `data:${img.mediaType};base64,${bytesToBase64(img.data)}` } }]
232
- } catch {
312
+ } catch (err) {
313
+ // 曾经这里是空 catch:宿主把请求目标从 maxPixels 换成 width/height 之后读图
314
+ // 全失败,图被静默换成占位文本,跨两个内测版本没被发现(issue #8)。降级仍然
315
+ // 保留(不能让整条消息失败),但原因必须出口。
316
+ log?.(`读取图片 ${ref.attachmentId} 失败,已降级为占位文本:${(err as Error).message}`)
233
317
  return [{ type: 'text', text: imagePlaceholder(ref.attachmentId) }]
234
318
  }
235
319
  }
@@ -250,8 +334,8 @@ function imagePlaceholder(attachmentId: string): string {
250
334
  }
251
335
 
252
336
  /** 组装 wire 请求体。图片序列化需要读 attachments,故为异步。 */
253
- async function wireRequest(options: GenerateOptions, attachments: RouterAttachmentStore | undefined): Promise<Record<string, unknown>> {
254
- const messages = await wireMessages(options, options.system, attachments)
337
+ async function wireRequest(options: GenerateOptions, attachments: RouterAttachmentStore | undefined, log?: (msg: string) => void): Promise<Record<string, unknown>> {
338
+ const messages = await wireMessages(options, options.system, attachments, log)
255
339
  const body: Record<string, unknown> = {
256
340
  model: options.model,
257
341
  messages,
@@ -555,22 +639,28 @@ export class RouterAdapter extends LlmAdapter {
555
639
  private readonly resolveAttachments: () => RouterAttachmentStore | undefined
556
640
  /** 流空闲超时(ms):上游这么久不吐字节就判 TIMEOUT。 */
557
641
  private readonly idleTimeoutMs: number
642
+ /** 降级/失败诊断的出口(宿主接 ctx.logger.warn)。缺省丢弃 —— 仅为测试可注入。 */
643
+ private readonly log: ((msg: string) => void) | undefined
558
644
 
559
645
  /**
560
646
  * @param idleTimeoutMs 流空闲超时;缺省用生产值(300s,对齐 pi-ai)。
561
647
  * 可注入只为测试能压到几百毫秒,生产不传。
648
+ * @param log 降级诊断出口。图片读不到时会降级成占位文本,原因走这里 ——
649
+ * 曾经那条路径是个空 `catch`,出问题跨版本都没人发现(issue #8)。
562
650
  */
563
651
  constructor(
564
652
  baseURL: string,
565
653
  source: RouterAdapterSource,
566
654
  resolveAttachments?: () => RouterAttachmentStore | undefined,
567
655
  idleTimeoutMs: number = STREAM_IDLE_TIMEOUT_MS,
656
+ log?: (msg: string) => void,
568
657
  ) {
569
658
  super()
570
659
  this.baseURL = baseURL
571
660
  this.source = source
572
661
  this.resolveAttachments = resolveAttachments ?? (() => undefined)
573
662
  this.idleTimeoutMs = idleTimeoutMs
663
+ this.log = log
574
664
  }
575
665
 
576
666
  providerInfo(provider: string): { id: string; name: string } {
@@ -617,7 +707,7 @@ export class RouterAdapter extends LlmAdapter {
617
707
  }
618
708
 
619
709
  async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
620
- const body = await wireRequest(options, this.resolveAttachments())
710
+ const body = await wireRequest(options, this.resolveAttachments(), this.log)
621
711
  const controller = new AbortController()
622
712
  const onAbort = (): void => controller.abort()
623
713
  options.signal?.addEventListener('abort', onAbort, { once: true })
@@ -632,6 +722,9 @@ export class RouterAdapter extends LlmAdapter {
632
722
  ...attributionHeaders(),
633
723
  'content-type': 'application/json',
634
724
  accept: 'text/event-stream',
725
+ // 宿主盖的会话身份转成内部头:核心据此把用量按会话归因,
726
+ // 供「最近命中」徽章按当前会话显示。宿主没给就不带(外部调用)。
727
+ ...options.sessionId === undefined ? {} : { 'x-dsh-router-session': String(options.sessionId) },
635
728
  },
636
729
  body: JSON.stringify(body),
637
730
  signal: controller.signal,