@henjicc/ai-sdk 0.6.1 → 0.7.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.
Files changed (128) hide show
  1. package/CHANGELOG.md +41 -27
  2. package/README.md +59 -46
  3. package/dist/capabilities/client.js +103 -86
  4. package/dist/capabilities/speech-recognition/bailian/realtime/module.js +42 -7
  5. package/dist/capabilities/speech-recognition/bailian/realtime/protocol.js +57 -15
  6. package/dist/capabilities/speech-recognition/volcengine/realtime/module.js +42 -10
  7. package/dist/capabilities/translation/bailian/module.js +41 -13
  8. package/dist/capabilities/translation/bailian/sse.d.ts +1 -1
  9. package/dist/capabilities/translation/bailian/sse.js +82 -54
  10. package/dist/capabilities/types.d.ts +4 -0
  11. package/dist/catalog/audio/tts-common.d.ts +5 -0
  12. package/dist/catalog/audio/tts-common.js +17 -0
  13. package/dist/catalog/bailian/cosyvoice-v3.5.model.d.ts +2 -0
  14. package/dist/catalog/bailian/cosyvoice-v3.5.model.js +40 -0
  15. package/dist/catalog/bailian/minimax-speech-2.8.model.d.ts +2 -0
  16. package/dist/catalog/bailian/minimax-speech-2.8.model.js +38 -0
  17. package/dist/catalog/bailian/qwen-audio-3.1-tts-flash.model.d.ts +2 -0
  18. package/dist/catalog/bailian/qwen-audio-3.1-tts-flash.model.js +35 -0
  19. package/dist/catalog/fal/eleven-v3-tts.model.d.ts +2 -0
  20. package/dist/catalog/fal/eleven-v3-tts.model.js +31 -0
  21. package/dist/catalog/fal/minimax-speech-2.8.model.d.ts +2 -0
  22. package/dist/catalog/fal/minimax-speech-2.8.model.js +39 -0
  23. package/dist/catalog/fal/qwen3-tts-1.7b.model.d.ts +2 -0
  24. package/dist/catalog/fal/qwen3-tts-1.7b.model.js +33 -0
  25. package/dist/catalog/index.js +116 -90
  26. package/dist/catalog/kie/eleven-turbo-2.5-tts.model.d.ts +2 -0
  27. package/dist/catalog/kie/eleven-turbo-2.5-tts.model.js +29 -0
  28. package/dist/catalog/kie/eleven-v3-dialogue.model.d.ts +2 -0
  29. package/dist/catalog/kie/eleven-v3-dialogue.model.js +46 -0
  30. package/dist/catalog/kie/gemini-3.1-flash-tts.model.d.ts +2 -0
  31. package/dist/catalog/kie/gemini-3.1-flash-tts.model.js +40 -0
  32. package/dist/catalog/ppio/minimax-speech.model.js +1 -3
  33. package/dist/catalog/siliconflow/cosyvoice2-tts.model.d.ts +2 -0
  34. package/dist/catalog/siliconflow/cosyvoice2-tts.model.js +27 -0
  35. package/dist/catalog/siliconflow/moss-ttsd-0.5.model.d.ts +2 -0
  36. package/dist/catalog/siliconflow/moss-ttsd-0.5.model.js +28 -0
  37. package/dist/catalog/volcengine-speech/seed-icl-2.0.model.d.ts +2 -0
  38. package/dist/catalog/volcengine-speech/seed-icl-2.0.model.js +26 -0
  39. package/dist/catalog/volcengine-speech/seed-tts-2.0.model.d.ts +2 -0
  40. package/dist/catalog/volcengine-speech/seed-tts-2.0.model.js +25 -0
  41. package/dist/llm/chat.js +6 -1
  42. package/dist/llm/streaming.js +131 -91
  43. package/dist/packs/models/bailian/cosyvoice-v3.5.d.ts +6 -0
  44. package/dist/packs/models/bailian/cosyvoice-v3.5.js +7 -0
  45. package/dist/packs/models/bailian/minimax-speech-2.8.d.ts +6 -0
  46. package/dist/packs/models/bailian/minimax-speech-2.8.js +7 -0
  47. package/dist/packs/models/bailian/qwen-audio-3.1-tts-flash.d.ts +6 -0
  48. package/dist/packs/models/bailian/qwen-audio-3.1-tts-flash.js +7 -0
  49. package/dist/packs/models/fal/eleven-v3-tts.d.ts +6 -0
  50. package/dist/packs/models/fal/eleven-v3-tts.js +7 -0
  51. package/dist/packs/models/fal/minimax-speech-2.8.d.ts +6 -0
  52. package/dist/packs/models/fal/minimax-speech-2.8.js +7 -0
  53. package/dist/packs/models/fal/qwen3-tts-1.7b.d.ts +6 -0
  54. package/dist/packs/models/fal/qwen3-tts-1.7b.js +7 -0
  55. package/dist/packs/models/kie/eleven-turbo-2.5-tts.d.ts +6 -0
  56. package/dist/packs/models/kie/eleven-turbo-2.5-tts.js +7 -0
  57. package/dist/packs/models/kie/eleven-v3-dialogue.d.ts +6 -0
  58. package/dist/packs/models/kie/eleven-v3-dialogue.js +7 -0
  59. package/dist/packs/models/kie/gemini-3.1-flash-tts.d.ts +6 -0
  60. package/dist/packs/models/kie/gemini-3.1-flash-tts.js +7 -0
  61. package/dist/packs/models/siliconflow/cosyvoice2-tts.d.ts +6 -0
  62. package/dist/packs/models/siliconflow/cosyvoice2-tts.js +7 -0
  63. package/dist/packs/models/siliconflow/moss-ttsd-0.5.d.ts +6 -0
  64. package/dist/packs/models/siliconflow/moss-ttsd-0.5.js +7 -0
  65. package/dist/packs/models/volcengine-speech/seed-icl-2.0.d.ts +6 -0
  66. package/dist/packs/models/volcengine-speech/seed-icl-2.0.js +7 -0
  67. package/dist/packs/models/volcengine-speech/seed-tts-2.0.d.ts +6 -0
  68. package/dist/packs/models/volcengine-speech/seed-tts-2.0.js +7 -0
  69. package/dist/packs/provider-adapters/siliconflow.d.ts +3 -0
  70. package/dist/packs/provider-adapters/siliconflow.js +5 -0
  71. package/dist/packs/provider-adapters/volcengine-speech.d.ts +3 -0
  72. package/dist/packs/provider-adapters/volcengine-speech.js +5 -0
  73. package/dist/packs/provider-packs/bailian.d.ts +1 -1
  74. package/dist/packs/provider-packs/bailian.js +6 -3
  75. package/dist/packs/provider-packs/fal.d.ts +1 -1
  76. package/dist/packs/provider-packs/fal.js +41 -38
  77. package/dist/packs/provider-packs/kie.d.ts +1 -1
  78. package/dist/packs/provider-packs/kie.js +33 -30
  79. package/dist/packs/provider-packs/siliconflow.d.ts +4 -0
  80. package/dist/packs/provider-packs/siliconflow.js +7 -0
  81. package/dist/packs/provider-packs/volcengine-speech.d.ts +4 -0
  82. package/dist/packs/provider-packs/volcengine-speech.js +7 -0
  83. package/dist/protocols/sse-events.d.ts +4 -0
  84. package/dist/protocols/sse-events.js +31 -0
  85. package/dist/providers/audio-data.d.ts +2 -0
  86. package/dist/providers/audio-data.js +21 -0
  87. package/dist/providers/bailian.js +30 -2
  88. package/dist/providers/index.js +4 -0
  89. package/dist/providers/metadata.js +8 -0
  90. package/dist/providers/siliconflow.d.ts +3 -0
  91. package/dist/providers/siliconflow.js +34 -0
  92. package/dist/providers/volcengine-speech.d.ts +3 -0
  93. package/dist/providers/volcengine-speech.js +143 -0
  94. package/dist/types/model.d.ts +4 -4
  95. package/dist/types/model.js +3 -1
  96. package/docs/consumers.md +22 -5
  97. package/docs/llm-adaptation/README.md +28 -3
  98. package/docs/llm-adaptation//344/276/233/345/272/224/345/225/206/DeepSeek.md +14 -14
  99. package/docs/llm-adaptation//344/276/233/345/272/224/345/225/206/Kimi.md +3 -3
  100. package/docs/llm-adaptation//344/276/233/345/272/224/345/225/206//345/260/217/347/261/263MiMo.md +42 -42
  101. package/docs/llm-adaptation//344/276/233/345/272/224/345/225/206//346/231/272/350/260/261GLM.md +2 -0
  102. package/docs/model-adaptation/Fun-ASR-Realtime/Fun-ASR-Realtime_/347/231/276/347/202/274.md +22 -2
  103. package/docs/model-adaptation/GLM-5.3-Flash/GLM-5.3-Flash_/346/231/272/350/260/261.md +6 -6
  104. package/docs/model-adaptation/GPT-Image-2.5/GPT-Image-2.5_APIMart.md +66 -66
  105. package/docs/model-adaptation/GPT-Image-2.5/GPT-Image-2.5_Fal.md +55 -55
  106. package/docs/model-adaptation/GPT-Image-2.5/GPT-Image-2.5_Grsai.md +53 -53
  107. package/docs/model-adaptation/GPT-Image-2.5/GPT-Image-2.5_KIE.md +55 -55
  108. package/docs/model-adaptation/HappyHorse-1.1/HappyHorse-1.1_KIE.md +27 -27
  109. package/docs/model-adaptation/IC-Light-v2/IC-Light-v2_Fal.md +14 -14
  110. package/docs/model-adaptation/Qwen-MT-Flash/Qwen-MT-Flash_/347/231/276/347/202/274.md +22 -1
  111. package/docs/model-adaptation/Qwen-MT-Lite/Qwen-MT-Lite_/347/231/276/347/202/274.md +2 -1
  112. package/docs/model-adaptation/Qwen-MT-Plus/Qwen-MT-Plus_/347/231/276/347/202/274.md +2 -1
  113. package/docs/model-adaptation/Qwen3-ASR-Flash-Realtime/Qwen3-ASR-Flash-Realtime_/347/231/276/347/202/274.md +25 -2
  114. package/docs/model-adaptation/README.md +14 -10
  115. package/docs/model-adaptation/SeedASR-2.0-Realtime/SeedASR-2.0-Realtime_/347/201/253/345/261/261/345/274/225/346/223/216.md +6 -2
  116. package/docs/model-adaptation/Seedream-5.0-Flash/Seedream-5.0-Flash_/347/201/253/345/261/261/345/274/225/346/223/216.md +18 -18
  117. package/docs/model-adaptation/TTS-/345/244/232/344/276/233/345/272/224/345/225/206-2026-09.md +55 -0
  118. package/docs/model-adaptation//344/276/233/345/272/224/345/225/206//346/264/276/346/254/247/344/272/221.md +9 -9
  119. package/docs/model-adaptation//344/276/233/345/272/224/345/225/206//347/201/253/345/261/261/345/274/225/346/223/216.md +11 -11
  120. package/docs/model-adaptation//344/276/233/345/272/224/345/225/206//347/231/276/347/202/274.md +31 -31
  121. package/docs/model-adaptation//344/276/233/345/272/224/345/225/206//347/241/205/345/237/272/346/265/201/345/212/250.md +67 -67
  122. package/docs/model-adaptation//346/226/207/346/241/243/351/207/207/351/233/206/346/211/213/345/206/214.md +8 -8
  123. package/examples/form-renderer/package.json +1 -1
  124. package/examples/llm-chat/README.md +8 -8
  125. package/examples/llm-chat/index.ts +2 -2
  126. package/examples/llm-chat/package.json +1 -1
  127. package/examples/minimal-node/package.json +1 -1
  128. package/package.json +258 -258
package/CHANGELOG.md CHANGED
@@ -1,33 +1,47 @@
1
1
  # Changelog
2
2
 
3
- ## 0.6.1 - 2026-09-24
3
+ ## 0.7.0 - 2026-09-27
4
4
 
5
- - 修复百炼 Fun Duplex 实时识别把无句首标记的空文本中间帧误判为无效响应的问题;覆盖 Qwen Audio 3.1 streaming 与 Fun-ASR 共享协议。
6
- - 保持缺失文字字段、空最终结果、全程无有效 final 的严格校验及连接释放。
7
-
8
- ## 0.6.0 - 2026-09-24
9
-
10
- - 新增 MiMo 2.6 Pro / Flash / Pro-UltraSpeed 能力;默认推荐 Pro / Flash,保留 2.5 ID(官方计划 10 月 21 日下线),不自动替换用户选择。
11
- - MiMo Chat / Responses 思考字段分离,兼容完整 reasoning_text 事件及历史思考回传,流中断不再伪装完成。
12
- - 新增大陆 GLM-5.3-FlashX 与官方价格;Flash / FlashX 开放 JSON 输出并保留思考。
13
- - 新增 KIE HappyHorse 1.1 文生、图生、多图参考模式,以及火山 Seedream 5.0 Flash 生成/编辑和图层拆分;价格按最新官方单价。
14
- - 百炼新增 Qwen-Audio 3.1 短音频、文件与实时 ASR,支持方言/即时热词与说话人输出;修复短音频 JSON 回退、上下文字段及文件声道数组。
15
- - Kimi K3 资料补齐缓存写入 TTL 价格。新增公开入口保持向后兼容,既有外部 ASR 消费方从 0.2.x 升级仍须满足 0.5.0 的宿主流式上传要求。
16
-
17
-
18
- ## 0.5.2 - 2026-09-21
19
-
20
- - `llm/streaming` 新增有类型定义的文本、JSON Object 与 JSON Schema 输出配置,并把它正式写入 Chat Completions 的 `response_format`。
21
- - 模型能力现在可声明结构化输出与思考模式能否并用;不支持或尚未核实的组合在网络请求前返回可解析错误码,Groq 流式 JSON Schema 按官方限制明确拒绝。
22
- - 新增 `maxOutputTokens`,OpenAI/Groq/MiMo 使用 `max_completion_tokens`,其余兼容端点沿用 `max_tokens`;省略时不再强制 4096,流结束用 `truncated` 标记 token 上限截断。
23
- - 最终发送体复用正式构建结果,`onRequestBuilt` 只收到观察快照;保留正文/思考分流、AbortSignal、可选总时限与轻量受限宿主边界。
24
-
25
- ## 0.5.1 - 2026-09-21
26
-
27
- - Grsai GPT Image 2.5 默认变体从维护中的 Flare 切换为可用的 Standard;显式 Flare/Sunburst 参数保持兼容,等待官方恢复公告。
28
- - 默认请求体与默认价格估算同步使用 Standard,避免界面未选择变体时仍把任务提交到维护通道。
29
-
30
- ## 0.5.0 - 2026-09-21
5
+ - 新增 Fal、百炼、KIE、硅基流动和火山语音的 13 个 TTS 模型入口;保留派欧云 MiniMax Speech 2.8,并按平台契约隐藏未证实的情感参数。
6
+ - 接入百炼 MiniMax Hex 音频、百炼 Qwen/CosyVoice 同步音频、硅基流动二进制音频及火山 V3 分块音频;输出统一交给现有媒体落盘边界。
7
+ - 有公开字符单价的模型提供估算;按音频输出 Token 计费或缺少可核实按量单价的模型不显示数字估算。
8
+
9
+ ## 0.6.2 - 2026-09-24
10
+
11
+ - 完善聊天与翻译 SSE 的事件边界和完成状态校验:保留合法空中间帧、用量与状态通知,提前结束、服务端错误和无效终态不再伪装成功;保留翻译增量/累积协议差异。
12
+ - 修复百炼实时 ASR 字段校验、Qwen 中间文字拼接和状态通知处理;保留 0.6.1 空中间帧兼容,诊断包含实际模型、协议及失败阶段。
13
+ - 收口共享实时会话的取消、超时、后台故障通知和资源释放;SDK 会话提供可选 result promise,旧自定义会话仍兼容。握手清理失败保留首个故障,火山服务错误默认脱敏。
14
+ - 阻止聊天及火山实时识别在取消后继续派发同一帧的后续结果;明确智谱推理网络错误与上下文截断,扩展通知不冒充完成证据。
15
+ - 增加脱敏事件序列回归并执行修复撤销验证,更新聊天示例的合法结束帧。未改变供应商配额、网络稳定性或模型识别质量。
16
+
17
+ ## 0.6.1 - 2026-09-24
18
+
19
+ - 修复百炼 Fun Duplex 实时识别把无句首标记的空文本中间帧误判为无效响应的问题;覆盖 Qwen Audio 3.1 streaming 与 Fun-ASR 共享协议。
20
+ - 保持缺失文字字段、空最终结果、全程无有效 final 的严格校验及连接释放。
21
+
22
+ ## 0.6.0 - 2026-09-24
23
+
24
+ - 新增 MiMo 2.6 Pro / Flash / Pro-UltraSpeed 能力;默认推荐 Pro / Flash,保留 2.5 ID(官方计划 10 月 21 日下线),不自动替换用户选择。
25
+ - MiMo Chat / Responses 思考字段分离,兼容完整 reasoning_text 事件及历史思考回传,流中断不再伪装完成。
26
+ - 新增大陆 GLM-5.3-FlashX 与官方价格;Flash / FlashX 开放 JSON 输出并保留思考。
27
+ - 新增 KIE HappyHorse 1.1 文生、图生、多图参考模式,以及火山 Seedream 5.0 Flash 生成/编辑和图层拆分;价格按最新官方单价。
28
+ - 百炼新增 Qwen-Audio 3.1 短音频、文件与实时 ASR,支持方言/即时热词与说话人输出;修复短音频 JSON 回退、上下文字段及文件声道数组。
29
+ - Kimi K3 资料补齐缓存写入 TTL 价格。新增公开入口保持向后兼容,既有外部 ASR 消费方从 0.2.x 升级仍须满足 0.5.0 的宿主流式上传要求。
30
+
31
+
32
+ ## 0.5.2 - 2026-09-21
33
+
34
+ - `llm/streaming` 新增有类型定义的文本、JSON Object 与 JSON Schema 输出配置,并把它正式写入 Chat Completions 的 `response_format`。
35
+ - 模型能力现在可声明结构化输出与思考模式能否并用;不支持或尚未核实的组合在网络请求前返回可解析错误码,Groq 流式 JSON Schema 按官方限制明确拒绝。
36
+ - 新增 `maxOutputTokens`,OpenAI/Groq/MiMo 使用 `max_completion_tokens`,其余兼容端点沿用 `max_tokens`;省略时不再强制 4096,流结束用 `truncated` 标记 token 上限截断。
37
+ - 最终发送体复用正式构建结果,`onRequestBuilt` 只收到观察快照;保留正文/思考分流、AbortSignal、可选总时限与轻量受限宿主边界。
38
+
39
+ ## 0.5.1 - 2026-09-21
40
+
41
+ - Grsai GPT Image 2.5 默认变体从维护中的 Flare 切换为可用的 Standard;显式 Flare/Sunburst 参数保持兼容,等待官方恢复公告。
42
+ - 默认请求体与默认价格估算同步使用 Standard,避免界面未选择变体时仍把任务提交到维护通道。
43
+
44
+ ## 0.5.0 - 2026-09-21
31
45
 
32
46
  - 文件 ASR 支持分块 JSON/Base64 与 multipart,请求体不重组整文件;支持结构化大小错误、PCM 时长估算及取消清理。
33
47
  - **迁移要求**:百炼异步本地文件需宿主 transport.uploadFile 原生直传 OSS,不再走媒体读取通道。其他流式路径需 describe/readChunk + fetchStream;旧 read 路径保留但不得放宽内存保护。
package/README.md CHANGED
@@ -48,10 +48,10 @@ try {
48
48
 
49
49
  百炼工厂必须传 `baseUrl` 为实际地域/工作空间的 API 根地址,不包含端点路径。其他供应商可以覆盖根地址与 `credentialId`;SDK 不自动切换地域或账号。不同模型的向量不能混用;切换模型通常需要重建向量索引。Rerank 分数仅在本次请求内比较。
50
50
 
51
- SDK `0.6.1` 的正式分发渠道为公共 npm,无需配置 registry 或访问令牌:
51
+ SDK `0.7.0` 的正式分发渠道为公共 npm,无需配置 registry 或访问令牌:
52
52
 
53
53
  ```bash
54
- npm install @henjicc/ai-sdk@0.6.1
54
+ npm install @henjicc/ai-sdk@0.7.0
55
55
  ```
56
56
 
57
57
  然后提供 4 个宿主能力(`Transport` / `CredentialStore` / `MediaReader` / `Logger`),创建客户端:
@@ -192,50 +192,50 @@ import {
192
192
  type RuntimeContext,
193
193
  } from '@henjicc/ai-sdk/llm/streaming'
194
194
 
195
- const request: LlmChatRequestDto = {
196
- requestId: 'chat-1', providerId: 'openai', modelId: 'gpt-5-mini',
197
- messages: [{ role: 'user', content: '你好' }],
198
- capabilities: {
199
- structuredOutputMode: 'schema',
200
- reasoning: true,
201
- structuredOutputWithReasoning: true,
202
- maxOutputTokens: 32_000,
203
- },
204
- reasoning: { enabled: true, effort: 'high' },
205
- structuredOutput: {
206
- type: 'json_schema',
207
- name: 'subtitle_segments',
208
- strict: true,
209
- schema: {
210
- type: 'object',
211
- properties: {
212
- segments: {
213
- type: 'array',
214
- items: {
215
- type: 'object',
216
- properties: { text: { type: 'string' } },
217
- required: ['text'],
218
- additionalProperties: false,
219
- },
220
- },
221
- },
222
- required: ['segments'],
223
- additionalProperties: false,
224
- },
225
- },
226
- maxOutputTokens: 24_000,
227
- }
228
- const result = await runLlmChatStream(request, 'chat-1', onStreamEvent, runtime as RuntimeContext)
229
- console.log(result.output, result.reasoningOutput, result.usage, result.finishReason, result.truncated)
230
- cancelLlmChatTask('chat-1')
231
- ```
232
-
233
- `structuredOutput` 支持 `text`、`json_object`、`json_schema`。轻量入口只执行 Chat Completions,
234
- 所以这里会正式映射到最终请求体的 `response_format`;它不包含 Responses API 执行器,Responses
235
- 路径的等价字段 `text.format` 由完整 `llm` 入口处理。JSON Object、JSON Schema 和“结构化输出 +
236
- 思考”都必须由 `capabilities` 明确声明;未知或不支持的组合会在网络请求前返回结构化错误,不会静默
237
- 降级。省略 `maxOutputTokens` 时 SDK 不再强加 4096 tokens,而是交给供应商默认值;省略执行参数里的
238
- `timeoutMs` 则没有请求总时限,宿主可在 `Transport` 中实现长时间无数据才超时。
195
+ const request: LlmChatRequestDto = {
196
+ requestId: 'chat-1', providerId: 'openai', modelId: 'gpt-5-mini',
197
+ messages: [{ role: 'user', content: '你好' }],
198
+ capabilities: {
199
+ structuredOutputMode: 'schema',
200
+ reasoning: true,
201
+ structuredOutputWithReasoning: true,
202
+ maxOutputTokens: 32_000,
203
+ },
204
+ reasoning: { enabled: true, effort: 'high' },
205
+ structuredOutput: {
206
+ type: 'json_schema',
207
+ name: 'subtitle_segments',
208
+ strict: true,
209
+ schema: {
210
+ type: 'object',
211
+ properties: {
212
+ segments: {
213
+ type: 'array',
214
+ items: {
215
+ type: 'object',
216
+ properties: { text: { type: 'string' } },
217
+ required: ['text'],
218
+ additionalProperties: false,
219
+ },
220
+ },
221
+ },
222
+ required: ['segments'],
223
+ additionalProperties: false,
224
+ },
225
+ },
226
+ maxOutputTokens: 24_000,
227
+ }
228
+ const result = await runLlmChatStream(request, 'chat-1', onStreamEvent, runtime as RuntimeContext)
229
+ console.log(result.output, result.reasoningOutput, result.usage, result.finishReason, result.truncated)
230
+ cancelLlmChatTask('chat-1')
231
+ ```
232
+
233
+ `structuredOutput` 支持 `text`、`json_object`、`json_schema`。轻量入口只执行 Chat Completions,
234
+ 所以这里会正式映射到最终请求体的 `response_format`;它不包含 Responses API 执行器,Responses
235
+ 路径的等价字段 `text.format` 由完整 `llm` 入口处理。JSON Object、JSON Schema 和“结构化输出 +
236
+ 思考”都必须由 `capabilities` 明确声明;未知或不支持的组合会在网络请求前返回结构化错误,不会静默
237
+ 降级。省略 `maxOutputTokens` 时 SDK 不再强加 4096 tokens,而是交给供应商默认值;省略执行参数里的
238
+ `timeoutMs` 则没有请求总时限,宿主可在 `Transport` 中实现长时间无数据才超时。
239
239
 
240
240
  ### 可选模型分发包与统一能力筛选
241
241
 
@@ -493,6 +493,19 @@ await capabilities.dispose()
493
493
  - `llm/bigmodel`:智谱同一 provider family 下的中国大陆/Global 端点 profile、独立凭据槽与 GLM-5.3-Flash 能力。
494
494
  - `llm/modules`:外部包、插件与内置 LLM 共用的注册、执行、发现、取消和 namespace 卸载边界。
495
495
 
496
+ 实时会话仍使用 `openSession → send → finish/close`。由 client 创建的会话额外提供 `result` Promise:
497
+ 它不主动发送 finish;在最终结果到达或后台失败、取消、超时、主动关闭,并完成资源收尾后结算。
498
+ 调用方可以观察 `session.result` 以获知停止发送音频期间的后台失败,不必等到下一次 `send/finish`。
499
+ `finish()` 幂等且返回同一结果;正常关闭但没有最终结果时 `result` 拒绝 `realtime_session_closed`,
500
+ 取消是 `cancelled`,超时是 `timeout`,供应商失败保留原错误。未订阅该 Promise 不产生未处理拒绝,
501
+ SDK 仍按现有日志契约记录失败。
502
+
503
+ 外部实时驱动可以提供可选 `result` Promise,致命后台/发送错误必须拒绝它,成功则返回有效最终结果。
504
+ 输入校验等可恢复的单次 send 错误只拒绝该调用,不应结束 result。旧驱动无需新增字段即可继续使用;
505
+ 如果旧驱动没有提供结果通道,SDK 无法自行发现它的后台失败。公共类型中的可选属性用于兼容旧会话实现。
506
+ 完成、失败、取消与关闭竞争时只结算一次;清理失败不能覆盖首个失败,但成功路径清理失败不能报成功。
507
+ 连接、监听器与定时器回收后,原 requestId 可用于新会话;不会自动重连或重放已可能计费的音频。
508
+
496
509
  ASR module ID 固定为 `<providerId>.speech-recognition.<modelId>`,翻译固定为
497
510
  `bailian.translation.<modelId>`;供应商 ID 使用 `bailian` / `volcengine` / `siliconflow` / `groq`,
498
511
  没有 `funasr` 兼容供应商别名。火山文件标准版不会读取本地 bytes/media-ref;宿主必须先通过自己受控的
@@ -193,117 +193,134 @@ export function createCapabilityClient(config) {
193
193
  ? new AiRuntimeError('timeout', `Capability session timed out: ${requestId}`)
194
194
  : cancelledError(requestId);
195
195
  }
196
- let ended = false;
197
196
  let closePromise;
198
197
  let finishPromise;
198
+ let outcome;
199
+ let settlement;
200
+ let cleanupError;
201
+ const pendingSends = new Set();
202
+ let resolveResult;
203
+ let rejectResult;
204
+ const result = new Promise((resolve, reject) => { resolveResult = resolve; rejectResult = reject; });
205
+ // Observing is optional; the original promise remains rejected for callers.
206
+ void result.catch(() => undefined);
199
207
  const closeDriver = () => {
200
208
  closePromise ??= Promise.resolve().then(async () => await driver.close?.());
201
209
  return closePromise;
202
210
  };
203
- const end = (error) => {
204
- if (ended)
205
- return;
206
- ended = true;
207
- options.signal?.removeEventListener('abort', forwardAbort);
208
- if (timeout !== undefined)
209
- clearTimeout(timeout);
210
- active.delete(requestId);
211
- markFinished();
212
- span.end(error);
213
- };
214
- const closeAfterAbort = async () => {
215
- if (ended)
216
- return;
217
- const error = timedOut
218
- ? new AiRuntimeError('timeout', `Capability session timed out: ${requestId}`)
219
- : cancelledError(requestId);
220
- try {
221
- await closeDriver();
222
- }
223
- finally {
224
- runtime.logger.error('能力实时会话失败', {
225
- event: 'capability.session.failed', requestId, context: { capabilityId: id }, error,
226
- });
227
- end(error);
228
- }
229
- };
230
- controller.signal.addEventListener('abort', () => { void closeAfterAbort(); }, { once: true });
231
- const sessionInactiveError = () => timedOut
211
+ const abortError = () => timedOut
232
212
  ? new AiRuntimeError('timeout', `Capability session timed out: ${requestId}`)
233
213
  : cancelledError(requestId);
214
+ const normalize = (error) => controller.signal.aborted
215
+ ? abortError() : normalizeExecutionError(id, error);
216
+ const inactiveError = () => outcome && outcome.kind !== 'completed'
217
+ ? outcome.error : new AiRuntimeError('realtime_session_inactive', `Capability session is inactive: ${requestId}`);
218
+ const settle = (next) => {
219
+ if (settlement)
220
+ return settlement;
221
+ outcome = next;
222
+ for (const reject of pendingSends)
223
+ reject(inactiveError());
224
+ pendingSends.clear();
225
+ settlement = (async () => {
226
+ let terminal = next;
227
+ try {
228
+ await closeDriver();
229
+ }
230
+ catch (error) {
231
+ cleanupError = normalizeExecutionError(id, error);
232
+ // Keep the first failure/cancellation; failed cleanup cannot make
233
+ // a successful result or explicit close look successful.
234
+ if (terminal.kind !== 'failed')
235
+ terminal = { kind: 'failed', error: cleanupError };
236
+ }
237
+ outcome = terminal;
238
+ controller.signal.removeEventListener('abort', onAbort);
239
+ options.signal?.removeEventListener('abort', forwardAbort);
240
+ if (timeout !== undefined)
241
+ clearTimeout(timeout);
242
+ active.delete(requestId);
243
+ markFinished();
244
+ if (terminal.kind === 'failed') {
245
+ runtime.logger.error('能力实时会话失败', {
246
+ event: 'capability.session.failed', requestId,
247
+ context: { capabilityId: id, ...(cleanupError ? { cleanupErrorCode: cleanupError.code } : {}) },
248
+ error: terminal.error,
249
+ });
250
+ }
251
+ else {
252
+ runtime.logger.info(terminal.kind === 'completed' ? '能力实时会话完成' : '能力实时会话关闭', {
253
+ event: `capability.session.${terminal.kind}`, requestId, context: { capabilityId: id },
254
+ });
255
+ }
256
+ span.end(terminal.kind === 'failed' ? terminal.error : undefined);
257
+ if (terminal.kind === 'completed')
258
+ resolveResult(terminal.output);
259
+ else
260
+ rejectResult(terminal.error);
261
+ })();
262
+ return settlement;
263
+ };
264
+ const onAbort = () => { void settle({ kind: 'failed', error: abortError() }); };
265
+ controller.signal.addEventListener('abort', onAbort, { once: true });
266
+ if (driver.result) {
267
+ void driver.result.then(output => settle({ kind: 'completed', output }), error => settle({ kind: 'failed', error: normalize(error) }));
268
+ }
234
269
  return {
235
270
  requestId,
236
271
  descriptor: registered.module.descriptor,
272
+ result,
237
273
  send: async (value) => {
238
- if (ended || controller.signal.aborted)
239
- throw sessionInactiveError();
240
- await driver.send(value);
274
+ if (outcome || finishPromise)
275
+ throw inactiveError();
276
+ if (controller.signal.aborted)
277
+ throw abortError();
278
+ let rejectSend;
279
+ const pending = new Promise((resolve, reject) => {
280
+ rejectSend = reject;
281
+ pendingSends.add(reject);
282
+ void Promise.resolve().then(async () => {
283
+ if (outcome)
284
+ throw inactiveError();
285
+ await driver.send(value);
286
+ }).then(resolve, reject);
287
+ });
288
+ try {
289
+ await pending;
290
+ }
291
+ finally {
292
+ pendingSends.delete(rejectSend);
293
+ }
241
294
  },
242
295
  finish: () => {
243
296
  if (finishPromise)
244
297
  return finishPromise;
245
- if (ended || controller.signal.aborted)
246
- return Promise.reject(sessionInactiveError());
247
- finishPromise = (async () => {
248
- let output;
249
- try {
250
- output = await driver.finish();
251
- if (controller.signal.aborted)
252
- throw cancelledError(requestId);
253
- }
254
- catch (error) {
255
- const normalized = controller.signal.aborted
256
- ? cancelledError(requestId)
257
- : normalizeExecutionError(id, error);
298
+ finishPromise = result;
299
+ if (!outcome) {
300
+ void Promise.resolve().then(async () => {
301
+ if (outcome)
302
+ return;
258
303
  try {
259
- await closeDriver();
304
+ const output = await driver.finish();
305
+ if (controller.signal.aborted)
306
+ throw abortError();
307
+ await settle({ kind: 'completed', output });
260
308
  }
261
- catch {
262
- // 保留 finish 的首个失败;close 仅负责尽力释放连接。
309
+ catch (error) {
310
+ await settle({ kind: 'failed', error: normalize(error) });
263
311
  }
264
- runtime.logger.error('能力实时会话失败', {
265
- event: 'capability.session.failed', requestId, context: { capabilityId: id }, error: normalized,
266
- });
267
- end(normalized);
268
- throw normalized;
269
- }
270
- try {
271
- await closeDriver();
272
- }
273
- catch (error) {
274
- const normalized = normalizeExecutionError(id, error);
275
- runtime.logger.error('能力实时会话失败', {
276
- event: 'capability.session.failed', requestId, context: { capabilityId: id }, error: normalized,
277
- });
278
- end(normalized);
279
- throw normalized;
280
- }
281
- runtime.logger.info('能力实时会话完成', {
282
- event: 'capability.session.completed', requestId, context: { capabilityId: id },
283
312
  });
284
- end();
285
- return output;
286
- })();
313
+ }
287
314
  return finishPromise;
288
315
  },
289
316
  close: async () => {
290
- if (ended)
317
+ if (outcome) {
318
+ await settlement;
291
319
  return;
292
- try {
293
- await closeDriver();
294
- runtime.logger.info('能力实时会话关闭', {
295
- event: 'capability.session.closed', requestId, context: { capabilityId: id },
296
- });
297
- end();
298
- }
299
- catch (error) {
300
- const normalized = normalizeExecutionError(id, error);
301
- runtime.logger.error('能力实时会话失败', {
302
- event: 'capability.session.failed', requestId, context: { capabilityId: id }, error: normalized,
303
- });
304
- end(normalized);
305
- throw normalized;
306
320
  }
321
+ await settle({ kind: 'closed', error: new AiRuntimeError('realtime_session_closed', `Capability session was closed: ${requestId}`) });
322
+ if (cleanupError)
323
+ throw cleanupError;
307
324
  },
308
325
  };
309
326
  }
@@ -84,6 +84,8 @@ export function createBailianRealtimeAsrModule(preset, options = {}) {
84
84
  if (context.signal.aborted)
85
85
  throw cancelledError(context.requestId);
86
86
  const credential = await apiKey(context);
87
+ if (context.signal.aborted)
88
+ throw cancelledError(context.requestId);
87
89
  const transport = context.runtime.realtime;
88
90
  if (!transport)
89
91
  throw new AiRuntimeError('realtime_transport_missing', 'Host did not provide realtime transport');
@@ -118,21 +120,32 @@ async function openDriver(preset, input, taskId, connection, context) {
118
120
  let lastPartial = '';
119
121
  let durationMs;
120
122
  let terminalError;
123
+ let lastEventKind;
121
124
  const segments = [];
122
125
  const created = deferred();
123
126
  const ready = deferred();
124
127
  const finished = deferred();
128
+ const onAbort = () => fail(cancelledError(context.requestId));
125
129
  const closeConnection = () => {
130
+ context.signal.removeEventListener('abort', onAbort);
126
131
  closePromise ??= Promise.resolve().then(async () => await connection.close(1000, 'session complete'));
127
132
  return closePromise;
128
133
  };
129
134
  const fail = (error) => {
130
135
  if (phase === 'finished' || phase === 'closed' || phase === 'failed')
131
136
  return;
137
+ const failedPhase = phase;
132
138
  phase = 'failed';
133
- const normalized = error instanceof AiRuntimeError
139
+ const original = error instanceof AiRuntimeError
134
140
  ? error
135
- : new AiRuntimeError('provider_realtime_error', error instanceof Error ? error.message : 'Bailian realtime failed');
141
+ : new AiRuntimeError('provider_realtime_error', 'Realtime transport or event handler failed');
142
+ const message = original.message.replace(`[${original.code}] `, '');
143
+ const normalized = new AiRuntimeError(original.code, original.details?.modelId === preset.modelId ? message : `${preset.modelId} (${preset.protocol}): ${message}`, {
144
+ providerId: 'bailian', modelId: preset.modelId, protocol: preset.protocol,
145
+ stage: failedPhase, operation: original.details?.stage,
146
+ eventType: original.details?.eventType ?? lastEventKind,
147
+ providerCode: original.details?.providerCode,
148
+ });
136
149
  terminalError = normalized;
137
150
  created.reject(normalized);
138
151
  ready.reject(normalized);
@@ -159,6 +172,7 @@ async function openDriver(preset, input, taskId, connection, context) {
159
172
  await closeConnection();
160
173
  };
161
174
  const handle = async (event) => {
175
+ lastEventKind = event.kind;
162
176
  if (event.kind === 'created') {
163
177
  if (preset.protocol !== 'qwen-realtime' || phase !== 'opening') {
164
178
  throw new AiRuntimeError('protocol_event_out_of_order', 'Unexpected Bailian realtime created event');
@@ -206,7 +220,10 @@ async function openDriver(preset, input, taskId, connection, context) {
206
220
  return;
207
221
  }
208
222
  if (event.kind === 'error') {
209
- throw new AiRuntimeError('provider_task_failed', `${event.code ? `${event.code}: ` : ''}${event.message}`);
223
+ // Provider messages can echo request content. Retain the diagnostic code,
224
+ // never the full upstream message in the default error/logging path.
225
+ const providerCode = event.code && /^[\w.-]{1,80}$/.test(event.code) ? event.code : undefined;
226
+ throw new AiRuntimeError('provider_task_failed', 'Server reported a realtime task failure', { providerCode });
210
227
  }
211
228
  if (event.kind === 'ignored')
212
229
  return;
@@ -238,9 +255,10 @@ async function openDriver(preset, input, taskId, connection, context) {
238
255
  }
239
256
  };
240
257
  void consume();
241
- const onAbort = () => fail(cancelledError(context.requestId));
242
258
  context.signal.addEventListener('abort', onAbort, { once: true });
243
259
  try {
260
+ if (context.signal.aborted)
261
+ throw cancelledError(context.requestId);
244
262
  if (preset.protocol === 'fun-duplex') {
245
263
  await connection.send(buildFunStart(preset, input, taskId));
246
264
  }
@@ -252,10 +270,19 @@ async function openDriver(preset, input, taskId, connection, context) {
252
270
  }
253
271
  catch (error) {
254
272
  fail(error);
255
- await closeConnection();
256
- throw error;
273
+ try {
274
+ await closeConnection();
275
+ }
276
+ catch {
277
+ const primary = terminalError;
278
+ throw new AiRuntimeError(primary.code, primary.message.replace(`[${primary.code}] `, ''), {
279
+ ...primary.details, cleanupFailed: true,
280
+ });
281
+ }
282
+ throw terminalError ?? error;
257
283
  }
258
284
  return {
285
+ result: finished.promise,
259
286
  send: async (chunk) => {
260
287
  if (terminalError)
261
288
  throw terminalError;
@@ -263,7 +290,14 @@ async function openDriver(preset, input, taskId, connection, context) {
263
290
  throw new AiRuntimeError('realtime_session_inactive', 'Bailian realtime session is not active');
264
291
  if (chunk.bytes.byteLength === 0)
265
292
  return;
266
- await connection.send(preset.protocol === 'fun-duplex' ? chunk.bytes : buildQwenAudio(chunk.bytes));
293
+ const data = preset.protocol === 'fun-duplex' ? chunk.bytes : buildQwenAudio(chunk.bytes);
294
+ try {
295
+ await connection.send(data);
296
+ }
297
+ catch (error) {
298
+ fail(error);
299
+ throw terminalError ?? error;
300
+ }
267
301
  },
268
302
  finish: () => {
269
303
  if (finishPromise)
@@ -286,6 +320,7 @@ async function openDriver(preset, input, taskId, connection, context) {
286
320
  }
287
321
  return await finished.promise;
288
322
  })();
323
+ void finishPromise.catch(error => fail(error));
289
324
  return finishPromise;
290
325
  },
291
326
  close: async () => {