@acosmi/sdk-ts 2.15.0 → 2.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/CHANGELOG.md CHANGED
@@ -5,6 +5,41 @@ All notable changes to `@acosmi/sdk-ts` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.17.0] - 2026-08-15 — 桌面 loopback OAuth state 全路径闸 + 端口确定性关闭
9
+
10
+ **回环监听在整个登录期间对本机任意进程开放,state 是唯一的门。** 2.16.0 及更早版本的桌面 loopback `authorize` 不生成也不校验 OAuth `state`(`af2daa0b` 已在源码补上核心校验,但从未发版);且校验只覆盖"带 code"的回调 —— 携带 OAuth error 的回调**绕过 state 直接把登录结算成"用户已拒绝"**,本机恶意进程不猜任何秘密就能打断/塑形一次等待中的登录;重复 `state` 参数取首值即可蒙混(`?code=攻击者的码&state=<正确值>&state=x` 在旧实现下**成功登录到攻击者会话**)。本版把 state 校验提到 `/callback` 所有形态之前,并收紧为"恰好一个"。
11
+
12
+ ### Fixed
13
+
14
+ - **`/callback` 全形态 state 先行校验** —— 成功回调、OAuth error 回调、畸形回调一律先验 state 再消费 code / 结算 denied。缺失、重复(含重复的正确值)、错值均以稳定错误码 `state_mismatch` 拒绝本次登录、返回安全失败页并关闭 listener;错误信息只描述形态,不回显 code / state / token / 完整 callback query。
15
+ - **"恰好一个 state"** —— `searchParams.getAll('state').length === 1` 且严格等值;重复参数不再因 `get()` 取首值而通过。
16
+ - **端口确定性关闭** —— `finally` 里在 `server.close()` 后补 `closeIdleConnections?.()`:Node 18 上 `close()` 不关浏览器残留的 idle keep-alive 连接,端口会滞留到对端松手;现在每条终止路径(成功 / 各类 state 失败 / 用户拒绝 / 超时 / 取消 / 浏览器打开失败)都以端口完全关闭收尾。
17
+
18
+ ### 回归闸门
19
+
20
+ - `test/auth/desktop-loopback-state.test.ts` —— 十路终止矩阵:成功、缺失 state、错误 state、重复 state(异值与同值)、恶意先到回调后合法回调不能复活、用户拒绝(正确 state + OAuth error → `auth_denied`)、超时、取消(pre-aborted signal)、浏览器打开失败后手动回调仍可完成;每条路径断言**只结算一次**(事件流恰一条 error / 结果不可翻转)且**新连接最终被拒**(listener teardown)。对 2.16.0 实测:4 条缺口用例全红(其中"重复 state 首值正确"在旧实现下以攻击者 code 成功 resolve),6 条保行为用例双版本皆绿。
21
+
22
+ ## [2.16.0] - 2026-08-06 — chat 超时预算真正下传 + 流式活性回调
23
+
24
+ **一个预算只有在被传下去时才存在。** `chat` / `chatMessagesAnthropic` / `chatMessagesOpenAI` 三条推理链路都用 `withRequestTimeout(CHAT_REQUEST_TIMEOUT_MS)` 建了 11 分钟的外层预算,却只把 `ctl.signal` 传给 `doJSONFullRaw` —— 后者的 `timeoutMs` 形参有 **30 秒**默认值,会**另建**一个计时器并恒先于外层触发。于是 v1.6.0 那次"调整为 11min 以容纳 DeepSeek 保活窗口"的改动,**一天都没生效过**,而它上方的注释还在声称它已生效。
25
+
26
+ 这不是理论缺陷。生产网关单日 **29 条** latency 落在 27 831–30 046 ms 的 499(客户端主动断开),横跨 **4 个厂商 5 个模型**;受害最重的恰是各客户端的默认主循环模型 —— 也正是唯一拿到网关 11 分钟长首字节预算的那个 provider。它拿着最宽的服务端预算,死得最惨,因为杀它的计时器在客户端。真实用户主动取消的 latency 是随机的(1 294 / 1 575 ms),与这一串高度聚集的 30 秒在同一张表里一眼可分。
27
+
28
+ ### Fixed
29
+
30
+ - **`chat` / `chatMessagesAnthropic` / `chatMessagesOpenAI` / `generateVideo` 显式下传 `CHAT_REQUEST_TIMEOUT_MS`** —— 与同文件 `embeddings` / `rerank` / `generateImage` 三个一直正确的兄弟调用对齐。`generateVideo` 此前连外层预算都没有,同样被钉在 30 秒;现按其直接兄弟 `generateImage` 的形状修复(单次网络操作,不另加外层 controller)。
31
+ - **`Client.chat` 上方那条 v1.6.0 注释重写** —— 它描述的是一个从未交付的行为。注释与实测冲突时以实测为准,且必须同 PR 修掉;留着它就是给下一个读代码的人埋雷。
32
+
33
+ ### Added
34
+
35
+ - **`CHAT_REQUEST_TIMEOUT_MS` 导出** —— 供回归闸门按**符号**断言,避免测试抄字面量后与真源各自漂移(抄件断言恒绿 = 零覆盖)。消费方要更短的预算传 `signal` 即可,`withRequestTimeout` 取二者先到者。
36
+ - **`UpstreamActivityCallback` + `chatStream` / `chatMessagesStream` 的第 4 个可选实参 `onUpstreamActivity`** —— 消费方的流空闲看门狗判据是"多久没收到事件",但 SSE 上有两类**有字节、无事件**的情形对它完全失明:① 上游/网关推理前发的保活注释行被 `isSSECommentLine` 吞掉;② OpenAI 格式下 converter 对某些 data 行返回零事件。**心跳只有在抵达做判决的那一层时才叫心跳** —— 网关补心跳而 SDK 吞掉,等于没补。本回调对每一条 SSE 行触发一次(含注释行),早于任何过滤与解析;回调抛错被吞掉且不中断流(旁路信号不该有能力杀死主链路)。不传时行为逐字节不变。
37
+
38
+ ### 回归闸门
39
+
40
+ - `test/chat-timeout-budget.test.ts` —— 四条链路的**行为**断言(活过 30 秒、死在 `CHAT_REQUEST_TIMEOUT_MS`),外加一条 **default-deny 结构闸门**:`doJSONFullRaw` 的每个调用点必须显式传预算,控制面端点要豁免须进具名 allowlist 并写明理由。附负向对照,防扫描器坏掉后闸门恒绿。
41
+ - `test/stream-upstream-activity.test.ts` —— 判据是"第一个事件到达前已有 ≥3 次活性",只可能由 3 条保活注释行产生;刻意不用"活性次数 > 事件数",那条在注释行根本没进循环时会假绿。
42
+
8
43
  ## [2.15.0] - 2026-08-02 — sources 四态分类与零结果契约
9
44
 
10
45
  ### Added
package/README.md CHANGED
@@ -290,6 +290,30 @@ for await (const ev of stream) {
290
290
 
291
291
  `chatStreamWithUsage()` 返回带 usage/error/sources 标签的 AsyncIterable,便于聚合统计(详见 `src/core/client.ts`)。
292
292
 
293
+ ### 上游活性回调 `onUpstreamActivity`(v2.16+)
294
+
295
+ 如果你的集成层自带**流空闲看门狗**("多久没收到事件就判定连接死了"),必须接这个回调,否则看门狗会误杀健康连接。
296
+
297
+ SSE 上有两类**有字节、无事件**的情形,看门狗对它们完全失明:
298
+
299
+ 1. 上游(如 DeepSeek)与网关在开始推理前发的保活注释行 `: keep-alive` —— 按 SSE 规范它们不构成事件,被 `isSSECommentLine` 跳过;
300
+ 2. OpenAI 格式下 converter 对某些 data 行返回零事件。
301
+
302
+ 两种情形连接都健康、字节都在流动。**心跳只有在抵达做判决的那一层时才叫心跳** —— 网关补了心跳而 SDK 吞掉,等于没补。
303
+
304
+ ```ts
305
+ let lastActivity = Date.now();
306
+
307
+ const stream = client.chatMessagesStream(
308
+ modelId,
309
+ { messages, max_tokens: 4096 },
310
+ abortSignal,
311
+ () => { lastActivity = Date.now(); }, // ← 每条 SSE 行触发一次,含注释行
312
+ );
313
+ ```
314
+
315
+ 语义是「链路刚刚有动静」,不是「来了一个事件」;对每条 SSE 行触发一次,早于任何过滤与解析。回调抛出的错误会被吞掉且不中断流(旁路信号不该有能力杀死主链路)。不传时行为逐字节不变。
316
+
293
317
  ### Sources SSE 四态分类(v2.15+)
294
318
 
295
319
  `sources: []` 是检索成功但没有可展示引用的合法零结果,不是传输损坏。需要精确区分状态的新 consumer 使用加性接口 `classifySourcesEvent()`:
@@ -1100,7 +1124,8 @@ npm run docs # 经 TypeDoc 生成 API 参考到 docs/api/
1100
1124
 
1101
1125
  | 版本 | 状态 | 概要 |
1102
1126
  | --- | --- | --- |
1103
- | 2.15.0 | **当前版本** | **sources 四态分类与零结果契约(2026-08-02)**。加性新增 `classifySourcesEvent`、`SourcesEventParseResult` 与稳定 issue code,区分非 sources、合法空结果、有效结果和结构损坏;未知额外字段继续兼容。既有 `parseSourcesEvent` 的返回形状、宽松解析和 `null` 条件保持不变。 |
1127
+ | 2.16.0 | **当前版本** | **chat 超时预算真正下传 + 流式活性回调(2026-08-06)**。`chat` / `chatMessagesAnthropic` / `chatMessagesOpenAI` / `generateVideo` 此前漏传 `doJSONFullRaw` 的第 5 实参,内层 **30 秒**默认值恒先于外层 11 分钟预算触发 —— v1.6.0 那次"调整为 11min"一天都没生效过(生产实证:单日 29 条 latency≈30 000 ms 的 499,横跨 4 厂商 5 模型,受害最重的是默认主循环模型)。加性导出 `CHAT_REQUEST_TIMEOUT_MS`;`chatStream` / `chatMessagesStream` 新增第 4 个可选实参 `onUpstreamActivity`,让被 `isSSECommentLine` 吞掉的保活注释行(以及 OpenAI 格式下零事件的 data 行)能抵达消费方的空闲看门狗。不传回调时行为逐字节不变。 |
1128
+ | 2.15.0 | 稳定版 | **sources 四态分类与零结果契约(2026-08-02)**。加性新增 `classifySourcesEvent`、`SourcesEventParseResult` 与稳定 issue code,区分非 sources、合法空结果、有效结果和结构损坏;未知额外字段继续兼容。既有 `parseSourcesEvent` 的返回形状、宽松解析和 `null` 条件保持不变。 |
1104
1129
  | 2.14.0 | 稳定版 | **托管模型 input modalities 开放值域(2026-08-02)**。snake_case/camelCase 归一规则对称,数据字段允许未来新增标签;查询 API 仍保留已知标签自动补全。现网 camelCase 路径行为不变。 |
1105
1130
  | 2.13.0 | 稳定版 | **邀请奖励窗口重置券(2026-08-01)**。加性新增窗口重置券总览与幂等核销 API,不改变既有 token、额度和会员调用。 |
1106
1131
  | 2.12.0 | 稳定版 | **5 小时窗口软限额(2026-07-11)**。加性透传 `windowOverridable`、继续开关与结构化状态;老网关缺字段时按硬等待处理。 |
@@ -1123,7 +1148,7 @@ npm run docs # 经 TypeDoc 生成 API 参考到 docs/api/
1123
1148
  | 1.8.1 | 稳定版 | **enterprise 企业席位域落地(商品化总规划 P6a)**。新增 `client.enterprise.*`:`listMyEnterprises` / `getEnterprise` / `inviteMember` / `listEnterpriseMembers` / `listOrgSubscriptions` / `listSeats` / `assignSeat` / `revokeSeat` / `getOrgConsumeReport`。OWNER/ADMIN 权限下席位月度变更 ≤ 3 次(超出返 41xxx 业务码);订阅 + 席位 + 用量报表三视图齐备。 |
1124
1149
  | 1.8.0 | 稳定版 | **casehall 法律案件咨询域落地(商品化总规划 P5 方案 B)**。新增 `client.casehall.*`:`listLawyers` / `getLawyer` / `submitCaseLead` / `listMyCaseLeads` / `getMyCases` / `bookConsultation` / `listMyConsultations` / `listMyLegalOrders` / `listLegalSKUs`。律师库公开端点(VERIFIED + ACTIVE,PII L3 已脱敏)+ 案件线索 + 咨询 + 5 LEGAL_SERVICE SKU;admin 板块 9 模块不在 SDK 边界。 |
1125
1150
  | 1.7.0 | 稳定版 | **subscription + pricing + products 三域落地(商品化总规划 P1-P4)**。`subscription`:`listPlans` / `listUserSubscriptions`(订阅档位 + 用户订阅)。`pricing`:`getPricingConfig` / `quoteCompliance`(公开业务参数 + csign 合规 SKU 报价)。`products`:`getProductBySlug` / `listProductsByFamily` / `listComplianceSkus` / `listPublicModels`(商品中心 productFamily / audience / billingMode 索引)。 |
1126
- | 1.6.0 | 稳定版 | `ChatRequest.endUserId` 业务侧终端用户稳定标识,跨 provider 通用语义;SDK 自动按 wire-format 注入(OpenAI 顶层 `user_id` / Anthropic `metadata.user_id`);不传时网关从认证身份 HMAC-SHA256 自动派生 32 字符 id。`validateEndUserId(s)` helper 校验 PII / 长度 / 字符集。SSE keep-alive + 11 分钟超时调优;网关侧命中三项隔离能力(内容安全 / KV-cache / 调度)。 |
1151
+ | 1.6.0 | 稳定版 | `ChatRequest.endUserId` 业务侧终端用户稳定标识,跨 provider 通用语义;SDK 自动按 wire-format 注入(OpenAI 顶层 `user_id` / Anthropic `metadata.user_id`);不传时网关从认证身份 HMAC-SHA256 自动派生 32 字符 id。`validateEndUserId(s)` helper 校验 PII / 长度 / 字符集。SSE keep-alive 注释行显式跳过;网关侧命中三项隔离能力(内容安全 / KV-cache / 调度)。**订正(2026-08-06)**:本行原写"11 分钟超时调优",实际该次改动只建了外层 `AbortController`、未下传给 `doJSONFullRaw`,内层 30 秒默认值始终生效 —— **该调优直到 2.16.0 才真正落地**。 |
1127
1152
  | 1.5.1 | 历史稳定版 | **Docs / examples / 源码注释全量复核与修订 — 无 API 变化**。修补 8 项漂移与遗漏:README API 总览补 25+ 漏列方法(Chat 内部方法、Auth 浏览器 Web OAuth 4 原语、Skills/Notifications/Entitlements/Packages 全量、WS `connect/disconnect/isConnected`);重写 §"手动 OAuth" 段对齐 `auth.ts` 真实签名;§"双格式红线" + 三个 chat 示例 `maxTokens` → snake_case `max_tokens`;错误表补 `ModelNotFoundError`;§Agent Runs 补 13 类 stream event 完整表;新增 §`sanitize` 命名空间小节;`docs/compliance.md` 6 处 `Since v1.6/.../1.10` 统一为 `v1.5.0 (originally planned as ...)`;手册 §7 scope 数 12 → 15 + 新增 S1-S6 rollup 段;`examples/compliance-evidence-timestamp.ts` 补 `ScopeComplianceReportsWrite`(v1.3.2 漂移生产 401 隐患);`examples/auth-oauth-flow.ts` + `examples/core-chat.ts` 注释对齐当前契约;`src/index.ts` + `src/browser.ts` + `src/auth/auth.ts` 注释从 Go-port 语义改为"TS 主实现 + Web OAuth 替代品"。`typecheck` / `lint` / `vitest`(214) / `build` / `test:pack` 全绿。 |
1128
1153
  | 1.5.0 | 稳定版 | 沉淀 `src/shared/` 跨域共享 DTO(`PageRequest`/`PageResult` 别名、`OperationId`/`OperationStatus`/`IdempotencyKeyHeader`、`RetryAdvice` 叠加层、`PrincipalRef`/`TenantRef`、`FeatureGateStatus`/`StepUpStatus`/`BillingPreflightResult`)。**同时全量 rollup compliance gateway S1-S6** 能力(原 1.6.0-1.11.0 roadmap,见 [CHANGELOG.md](./CHANGELOG.md)):S1 6 个分页列表、S2 capabilities + operations 投影、S3 TSA 只读视图、S4 envelope 收尾 + void、S5 合同模板全生命周期 + 2 新 scope(`compliance:contract_template:{read,write}`)、S6 用印执行分页(`listSealUses`)。当前 compliance scope 总数 **15** 个(`complianceScopes()` 返回)。纯增量;8 个平台控制面占位命名空间仍待后端契约就绪后落地。 |
1129
1154
  | 1.4.2 | 稳定版 | `src/` 从扁平 36 文件按业务域重组为 per-domain 目录;公共导出符号集合、`exports`、`dist/` 路径一字未变(纯内部重组)。新增 TypeDoc API 文档。 |
@@ -1,7 +1,12 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __esm = (fn, res) => function __init() {
4
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
5
10
  };
6
11
  var __export = (target, all) => {
7
12
  for (var name in all)
@@ -1322,6 +1327,7 @@ async function authorize(meta, clientID, scopes, opts = {}) {
1322
1327
  };
1323
1328
  const verifier = await generateCodeVerifier();
1324
1329
  const challenge = await codeChallenge(verifier);
1330
+ const state = await generateState();
1325
1331
  const http = await import('http');
1326
1332
  const server = http.createServer();
1327
1333
  await new Promise((resolve, reject) => {
@@ -1337,6 +1343,8 @@ async function authorize(meta, clientID, scopes, opts = {}) {
1337
1343
  codeResolver = resolve;
1338
1344
  codeRejecter = reject;
1339
1345
  });
1346
+ codePromise.catch(() => {
1347
+ });
1340
1348
  server.on("request", (req, res) => {
1341
1349
  const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
1342
1350
  if (url.pathname !== "/callback") {
@@ -1344,6 +1352,16 @@ async function authorize(meta, clientID, scopes, opts = {}) {
1344
1352
  res.end();
1345
1353
  return;
1346
1354
  }
1355
+ const states = url.searchParams.getAll("state");
1356
+ const stateFailure = states.length === 0 ? "callback missing state" : states.length > 1 ? "callback carried multiple state values" : states[0] !== state ? "callback state does not match pending state" : null;
1357
+ if (stateFailure !== null) {
1358
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1359
+ res.end(
1360
+ `<!DOCTYPE html><html><head><meta charset="utf-8"><title>\u6388\u6743\u5931\u8D25</title></head><body style="font-family:system-ui,sans-serif;text-align:center;padding:60px 20px"><h2>\u6388\u6743\u5931\u8D25</h2><p>\u56DE\u8C03\u6821\u9A8C\u672A\u901A\u8FC7, \u5DF2\u4E2D\u6B62\u767B\u5F55\u3002</p><p style="color:#888;font-size:14px">\u53EF\u4EE5\u5173\u95ED\u6B64\u7A97\u53E3\u3002</p></body></html>`
1361
+ );
1362
+ codeRejecter(new Error(`authorize: ${ErrStateMismatch}: ${stateFailure} (possible CSRF)`));
1363
+ return;
1364
+ }
1347
1365
  const code = url.searchParams.get("code");
1348
1366
  if (!code) {
1349
1367
  const errMsg = url.searchParams.get("error_description") || url.searchParams.get("error") || "";
@@ -1374,6 +1392,7 @@ async function authorize(meta, clientID, scopes, opts = {}) {
1374
1392
  authURL.searchParams.set("response_type", "code");
1375
1393
  authURL.searchParams.set("code_challenge", challenge);
1376
1394
  authURL.searchParams.set("code_challenge_method", "S256");
1395
+ authURL.searchParams.set("state", state);
1377
1396
  if (scopes.length > 0) {
1378
1397
  authURL.searchParams.set("scope", scopes.join(" "));
1379
1398
  }
@@ -1411,7 +1430,9 @@ async function authorize(meta, clientID, scopes, opts = {}) {
1411
1430
  return { result: { code, redirectURI }, verifier };
1412
1431
  } catch (e) {
1413
1432
  const msg = e instanceof Error ? e.message : String(e);
1414
- if (msg.includes("denied")) {
1433
+ if (msg.includes(ErrStateMismatch)) {
1434
+ emit({ type: EventError, err_code: ErrStateMismatch, error: msg });
1435
+ } else if (msg.includes("denied")) {
1415
1436
  emit({ type: EventError, err_code: ErrAuthDenied, error: msg });
1416
1437
  } else if (msg.includes("timed out")) {
1417
1438
  emit({ type: EventError, err_code: ErrTimeout, error: msg });
@@ -1422,6 +1443,7 @@ async function authorize(meta, clientID, scopes, opts = {}) {
1422
1443
  } finally {
1423
1444
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
1424
1445
  server.close();
1446
+ server.closeIdleConnections?.();
1425
1447
  }
1426
1448
  }
1427
1449
  function htmlEscape(s) {
@@ -2288,6 +2310,13 @@ var ErrOAuthCORSBlocked = "oauth_cors_blocked";
2288
2310
  var ErrRefreshProxyFailed = "refresh_proxy_failed";
2289
2311
  var ErrTokenExpired = "token_expired";
2290
2312
  var CHAT_REQUEST_TIMEOUT_MS = 11 * 60 * 1e3;
2313
+ function notifyUpstreamActivity(cb) {
2314
+ if (!cb) return;
2315
+ try {
2316
+ cb();
2317
+ } catch {
2318
+ }
2319
+ }
2291
2320
  var DEFAULT_API_TIMEOUT_MS = 6e4;
2292
2321
  function newDeferred() {
2293
2322
  let resolve;
@@ -3001,7 +3030,13 @@ var Client = class _Client {
3001
3030
  try {
3002
3031
  const { body, adapter } = await this.buildChatRequest(modelID, r, ctl.signal);
3003
3032
  const endpoint = `/managed-models/${encodeURIComponent(modelID)}${adapter.endpointSuffix()}`;
3004
- const { result, headers } = await this.doJSONFullRaw("POST", endpoint, body, ctl.signal);
3033
+ const { result, headers } = await this.doJSONFullRaw(
3034
+ "POST",
3035
+ endpoint,
3036
+ body,
3037
+ ctl.signal,
3038
+ CHAT_REQUEST_TIMEOUT_MS
3039
+ );
3005
3040
  const resp = adapter.parseResponse(result);
3006
3041
  const v1 = headers.get("X-Token-Remaining");
3007
3042
  if (v1) {
@@ -3066,7 +3101,13 @@ var Client = class _Client {
3066
3101
  */
3067
3102
  async generateVideo(modelID, req, signal) {
3068
3103
  const endpoint = `/managed-models/${encodeURIComponent(modelID)}/videos/generations`;
3069
- const { result } = await this.doJSONFullRaw("POST", endpoint, req, signal);
3104
+ const { result } = await this.doJSONFullRaw(
3105
+ "POST",
3106
+ endpoint,
3107
+ req,
3108
+ signal,
3109
+ CHAT_REQUEST_TIMEOUT_MS
3110
+ );
3070
3111
  return this.unwrapAPIResponse(result);
3071
3112
  }
3072
3113
  /**
@@ -3135,7 +3176,8 @@ var Client = class _Client {
3135
3176
  "POST",
3136
3177
  `/managed-models/${encodeURIComponent(modelID)}/anthropic`,
3137
3178
  data,
3138
- ctl.signal
3179
+ ctl.signal,
3180
+ CHAT_REQUEST_TIMEOUT_MS
3139
3181
  );
3140
3182
  const rawStr = new TextDecoder().decode(result);
3141
3183
  try {
@@ -3169,7 +3211,13 @@ var Client = class _Client {
3169
3211
  const body = adapter.buildRequestBody(caps, r);
3170
3212
  const data = JSON.stringify(body);
3171
3213
  const endpoint = `/managed-models/${encodeURIComponent(modelID)}${adapter.endpointSuffix()}`;
3172
- const { result } = await this.doJSONFullRaw("POST", endpoint, data, ctl.signal);
3214
+ const { result } = await this.doJSONFullRaw(
3215
+ "POST",
3216
+ endpoint,
3217
+ data,
3218
+ ctl.signal,
3219
+ CHAT_REQUEST_TIMEOUT_MS
3220
+ );
3173
3221
  const { parseOpenAIResponseToAnthropic: parseOpenAIResponseToAnthropic2 } = await Promise.resolve().then(() => (init_openai(), openai_exports));
3174
3222
  return parseOpenAIResponseToAnthropic2(result);
3175
3223
  } finally {
@@ -3179,23 +3227,27 @@ var Client = class _Client {
3179
3227
  /**
3180
3228
  * 流式聊天 (SSE), 通过 async generator 返回事件
3181
3229
  * v0.5.0: 根据 adapter 路由端点
3230
+ *
3231
+ * @param onUpstreamActivity 见 {@link UpstreamActivityCallback}
3182
3232
  */
3183
- chatStream(modelID, req, signal) {
3233
+ chatStream(modelID, req, signal, onUpstreamActivity) {
3184
3234
  return {
3185
- [Symbol.asyncIterator]: () => this.chatStreamGen(modelID, req, signal, false)
3235
+ [Symbol.asyncIterator]: () => this.chatStreamGen(modelID, req, signal, false, onUpstreamActivity)
3186
3236
  };
3187
3237
  }
3188
3238
  /**
3189
3239
  * Anthropic 原生格式流式聊天 (SSE)
3190
3240
  * 调用 POST /managed-models/:id/anthropic, SSE 事件为 Anthropic 协议格式
3191
3241
  * 无 started/settled/failed 自定义事件, 无 data: [DONE], message_stop 为自然结束
3242
+ *
3243
+ * @param onUpstreamActivity 见 {@link UpstreamActivityCallback}
3192
3244
  */
3193
- chatMessagesStream(modelID, req, signal) {
3245
+ chatMessagesStream(modelID, req, signal, onUpstreamActivity) {
3194
3246
  return {
3195
- [Symbol.asyncIterator]: () => this.chatMessagesStreamGen(modelID, req, signal, false)
3247
+ [Symbol.asyncIterator]: () => this.chatMessagesStreamGen(modelID, req, signal, false, onUpstreamActivity)
3196
3248
  };
3197
3249
  }
3198
- async *chatStreamGen(modelID, req, signal, retried) {
3250
+ async *chatStreamGen(modelID, req, signal, retried, onUpstreamActivity) {
3199
3251
  const r = { ...req, stream: true };
3200
3252
  const { body, adapter } = await this.buildChatRequest(modelID, r, signal);
3201
3253
  const token = await this.ensureToken(signal);
@@ -3228,7 +3280,7 @@ var Client = class _Client {
3228
3280
  `stream: unauthorized and refresh failed: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`
3229
3281
  );
3230
3282
  }
3231
- yield* this.chatStreamGen(modelID, req, signal, true);
3283
+ yield* this.chatStreamGen(modelID, req, signal, true, onUpstreamActivity);
3232
3284
  return;
3233
3285
  }
3234
3286
  if (!resp.ok) {
@@ -3244,6 +3296,7 @@ var Client = class _Client {
3244
3296
  }
3245
3297
  let currentEvent = "";
3246
3298
  for await (const line of iterSSELines(resp.body)) {
3299
+ notifyUpstreamActivity(onUpstreamActivity);
3247
3300
  if (isSSECommentLine(line)) continue;
3248
3301
  if (line.startsWith("event:")) {
3249
3302
  currentEvent = line.slice("event:".length).trim();
@@ -3264,7 +3317,7 @@ var Client = class _Client {
3264
3317
  }
3265
3318
  }
3266
3319
  }
3267
- async *chatMessagesStreamGen(modelID, req, signal, retried) {
3320
+ async *chatMessagesStreamGen(modelID, req, signal, retried, onUpstreamActivity) {
3268
3321
  const r = { ...req, stream: true };
3269
3322
  const { body, adapter } = await this.buildChatRequest(modelID, r, signal);
3270
3323
  const token = await this.ensureToken(signal);
@@ -3297,7 +3350,7 @@ var Client = class _Client {
3297
3350
  `messages stream: unauthorized and refresh failed: ${refreshErr instanceof Error ? refreshErr.message : String(refreshErr)}`
3298
3351
  );
3299
3352
  }
3300
- yield* this.chatMessagesStreamGen(modelID, req, signal, true);
3353
+ yield* this.chatMessagesStreamGen(modelID, req, signal, true, onUpstreamActivity);
3301
3354
  return;
3302
3355
  }
3303
3356
  if (!resp.ok) {
@@ -3310,6 +3363,7 @@ var Client = class _Client {
3310
3363
  if (adapter.format() === 1 /* OpenAI */) {
3311
3364
  const converter = newOpenAIStreamConverter();
3312
3365
  for await (const line of iterSSELines(resp.body)) {
3366
+ notifyUpstreamActivity(onUpstreamActivity);
3313
3367
  if (isSSECommentLine(line)) continue;
3314
3368
  if (line.startsWith("event:")) {
3315
3369
  line.slice("event:".length).trim();
@@ -3324,6 +3378,7 @@ var Client = class _Client {
3324
3378
  const blockTypeMap = /* @__PURE__ */ new Map();
3325
3379
  let currentEvent = "";
3326
3380
  for await (const line of iterSSELines(resp.body)) {
3381
+ notifyUpstreamActivity(onUpstreamActivity);
3327
3382
  if (isSSECommentLine(line)) continue;
3328
3383
  if (line.startsWith("event:")) {
3329
3384
  currentEvent = line.slice("event:".length).trim();
@@ -3456,7 +3511,19 @@ var Client = class _Client {
3456
3511
  ctl.dispose();
3457
3512
  }
3458
3513
  }
3459
- /** doJSONFull 的 raw bytes 变体 (chat 用, 不立即 JSON.parse) */
3514
+ /**
3515
+ * doJSONFull 的 raw bytes 变体 (chat 用, 不立即 JSON.parse)
3516
+ *
3517
+ * ⚠️ 契约 (2026-08-06): `timeoutMs` 的 30s 默认值只适用于**控制面**端点 (列目录 /
3518
+ * 查配额 / 取任务状态)。凡走**模型推理或生成**的端点 —— chat / anthropic /
3519
+ * embeddings / rerank / images / videos —— 一律必须显式传入 `CHAT_REQUEST_TIMEOUT_MS`,
3520
+ * 哪怕调用方已经用 `withRequestTimeout` 建了外层预算: 外层只约束 `signal`, 内层
3521
+ * 会**另建**一个 `timeoutMs` 计时器, 30s 恒先于任何更长的外层预算触发。
3522
+ *
3523
+ * 这不是假设 —— 2026-08-06 事故里 chat 路径正因漏传本参数而被恒定钉死在 30s,
3524
+ * 且上方 v1.6.0 的注释还声称它已是 11min。回归闸门见
3525
+ * `tests/chat-timeout-budget.test.ts`。
3526
+ */
3460
3527
  async doJSONFullRaw(method, path, body, signal, timeoutMs = 3e4) {
3461
3528
  return this.doJSONFullRawInternal(method, path, body, signal, false, timeoutMs);
3462
3529
  }
@@ -4124,6 +4191,7 @@ var ScopeChatBridge = "chat_bridge";
4124
4191
  var ScopeChatBridgeRead = "chat_bridge:read";
4125
4192
  var ScopeChatBridgeWrite = "chat_bridge:write";
4126
4193
  var ScopeChatBridgeRotate = "chat_bridge:rotate";
4194
+ var ScopeAgentAccessManage = "agent_access:manage";
4127
4195
  var ScopeModels = "models";
4128
4196
  var ScopeModelsChat = "models:chat";
4129
4197
  var ScopeEntitlements = "entitlements";
@@ -4152,6 +4220,9 @@ function remoteControlScopes() {
4152
4220
  function chatBridgeScopes() {
4153
4221
  return [ScopeChatBridge];
4154
4222
  }
4223
+ function agentAccessScopes() {
4224
+ return [ScopeAgentAccessManage];
4225
+ }
4155
4226
 
4156
4227
  // src/models/index.ts
4157
4228
  init_types();
@@ -7117,6 +7188,15 @@ function sleep2(ms, signal) {
7117
7188
  }
7118
7189
 
7119
7190
  // src/support/bug-report.ts
7191
+ function unwrapBugReport(raw, op, isComplete) {
7192
+ if (raw && isComplete(raw)) return raw;
7193
+ const inner = raw?.data;
7194
+ if (inner && isComplete(inner)) return inner;
7195
+ const keys = raw && typeof raw === "object" ? Object.keys(raw) : [];
7196
+ throw new Error(
7197
+ `acosmi: ${op}: gateway accepted the request but the response is missing required fields (observed keys: ${keys.length > 0 ? keys.join(",") : "<none>"})`
7198
+ );
7199
+ }
7120
7200
  Client.prototype.submitBugReport = async function(reportData, signal) {
7121
7201
  if (reportData == null) {
7122
7202
  throw new Error("acosmi: reportData required");
@@ -7133,7 +7213,11 @@ Client.prototype.submitBugReport = async function(reportData, signal) {
7133
7213
  { content: contentStr },
7134
7214
  signal
7135
7215
  );
7136
- return result.data;
7216
+ return unwrapBugReport(
7217
+ result,
7218
+ "submitBugReport",
7219
+ (r) => typeof r.feedback_id === "string" && r.feedback_id.length > 0
7220
+ );
7137
7221
  };
7138
7222
  Client.prototype.getBugReport = async function(bugID, signal) {
7139
7223
  const trimmed = bugID.trim();
@@ -7146,7 +7230,7 @@ Client.prototype.getBugReport = async function(bugID, signal) {
7146
7230
  null,
7147
7231
  signal
7148
7232
  );
7149
- return resp.data;
7233
+ return unwrapBugReport(resp, "getBugReport", (r) => typeof r.id === "string");
7150
7234
  };
7151
7235
 
7152
7236
  // src/subscription/client.ts
@@ -7728,6 +7812,6 @@ function brandCredential(c) {
7728
7812
  return c;
7729
7813
  }
7730
7814
 
7731
- export { AGENT_RUN_META_TITLE, AGENT_RUN_META_WORKSPACE, ALL_INTEGRATION_STATUS, ALL_PLATFORMS, ALL_REGIONS, AgentRunStreamError, AgentRunsClient, AnthropicAdapter, AudienceEnum, BillingModeEnum, BucketClassCommercial, BucketClassGeneric, BusinessError, ChatBridgeClient, Client, ComplianceClient, CompliancePollError, CrabCodeByokClient, DEFAULT_API_TIMEOUT_MS, DEFAULT_GATEWAY_BASE_URL, LocalStorageTokenStore as DefaultBrowserTokenStore, DefaultRetryPolicy, ErrAuthDenied, ErrBillingCallbackCannotCommit, ErrBillingCommitRequiresLocalVerify, ErrBillingS2sForbidden, ErrBrowserOpen, ErrComplianceStepUpRequired, ErrDiscovery, ErrEnvelopeGateClosed, ErrOAuthCORSBlocked, ErrProviderNotConfigured, ErrProviderUnknownNoRetry, ErrRefreshProxyFailed, ErrRegistration, ErrSSLProxy, ErrSealApprovalContractHashMismatch, ErrSealApprovalExpired, ErrSealApprovalLocationMismatch, ErrSealApprovalNonceUsed, ErrSealApprovalNotApproved, ErrSealApprovalSealMismatch, ErrSealApprovalTransactorMismatch, ErrSealUseAlreadyConsumed, ErrStateMismatch, ErrTimeout, ErrTokenExchange, ErrTokenExpired, EventAuthURL, EventComplete, EventError, FileTokenStore, FilterStatusAdminBypass, FilterStatusDisabledByFlag, FilterStatusFallbackMissingUser, FilterStatusFallbackNoBuckets, FilterStatusFallbackTkdistError, FilterStatusFallbackTkdistSkew, FilterStatusInternalBypass, FilterStatusOK, FilterStatusUnknown, HTTPError, IdempotencyKeyHeader, InMemoryTokenStore, LocalStorageTokenStore, ModelNotFoundError, NetworkError, OAuthTokenEndpointError, OpenAIAdapter, OrderTerminalError, ProductFamilyEnum, ProviderFormat, RETRY_ADVICE_REASONS, RateLimitError, RegionScopeEnum, ScopeAI, ScopeAccount, ScopeChatBridge, ScopeChatBridgeRead, ScopeChatBridgeRotate, ScopeChatBridgeWrite, ScopeComplianceContractSigningRead, ScopeComplianceContractSigningWrite, ScopeComplianceContractTemplateRead, ScopeComplianceContractTemplateWrite, ScopeComplianceEvidenceRead, ScopeComplianceEvidenceWrite, ScopeComplianceReportsPublish, ScopeComplianceReportsRead, ScopeComplianceReportsWrite, ScopeComplianceSealApprovalApprove, ScopeComplianceSealApprovalRequest, ScopeComplianceSealManage, ScopeComplianceSealUseExecute, ScopeComplianceTimestampIssue, ScopeComplianceTimestampVerify, ScopeEntitlements, ScopeModels, ScopeModelsChat, ScopeProfile, ScopeRemoteControl, ScopeRemoteControlAgentRun, ScopeRemoteControlPermissionResponse, ScopeRemoteControlSessionControl, ScopeSkillStore, ScopeSkills, ScopeTokenPackages, ScopeTools, ScopeToolsExecute, ScopeWallet, ScopeWalletReadonly, ServerToolTypeWebSearch, StreamError, ThinkingHigh, ThinkingHighMinMaxTokens, ThinkingMax, ThinkingMaxFallbackMaxTokens, ThinkingOff, allScopes, anthropicResponseTextContent, anthropicResponseThinkingContent, anthropicResponseToolUseBlocks, apiResponseBusinessError, apiResponseGetMessage, asCredentialRef, authorize, bucketInfoIsCommercial, bucketRowIsCommercial, buildBetas, chatBridgeScopes, classifyComplianceError, classifySourcesEvent, commerceScopes, completeWebAuthorizationRequest, complianceErrorToRetryAdvice, complianceScopes, computeBackoff, createWebAuthorizationRequest, defaultRetryable, defaultSafeToRetry, discover, discoverWebOAuthMetadata, discoverWithProfile, effectivePolicy, exchangeCode, extractAnthropicBlockMeta, fileLockDefaults, findDesktopVisualUnderstandingModel, findFirstModelByInputModality, generateState, getAdapter, getAdapterForModel, getWindowLimitStreamDetails, isBillingConfirmable, isChannelInboundEvent, isComplianceBusinessError, isComplianceTerminalError, isContinuableWindowLimitError, isIntegrationStatus, isInvalidGrantError, isPlatform, isRegion, isSSECommentLine, isSSLError, isTerminalRemoteEvent, isValidTokenSet, isWindowLimitError, isWindowLimitStreamError, maxEndUserIdLength, modelScopes, modelSupportsImageInput, modelSupportsInputModality, newFileTokenStore, newThinkingConfig, newTokenSet, newWebSearchTool, normalizeGatewayBaseURL, normalizeOverrideBaseURL, parseNotificationEvent, parseRemoteControlEvent, parseSettlement, parseSourcesEvent, refreshToken, register, registerWebOAuthClient, remoteControlScopes, retryReasonForComplianceKey, retryReasonForOAuthError, revokeToken, sanitize_exports as sanitize, skillScopes, tokenSetIsExpired, uniqueMerge, validateEndUserId };
7815
+ export { AGENT_RUN_META_TITLE, AGENT_RUN_META_WORKSPACE, ALL_INTEGRATION_STATUS, ALL_PLATFORMS, ALL_REGIONS, AgentRunStreamError, AgentRunsClient, AnthropicAdapter, AudienceEnum, BillingModeEnum, BucketClassCommercial, BucketClassGeneric, BusinessError, CHAT_REQUEST_TIMEOUT_MS, ChatBridgeClient, Client, ComplianceClient, CompliancePollError, CrabCodeByokClient, DEFAULT_API_TIMEOUT_MS, DEFAULT_GATEWAY_BASE_URL, LocalStorageTokenStore as DefaultBrowserTokenStore, DefaultRetryPolicy, ErrAuthDenied, ErrBillingCallbackCannotCommit, ErrBillingCommitRequiresLocalVerify, ErrBillingS2sForbidden, ErrBrowserOpen, ErrComplianceStepUpRequired, ErrDiscovery, ErrEnvelopeGateClosed, ErrOAuthCORSBlocked, ErrProviderNotConfigured, ErrProviderUnknownNoRetry, ErrRefreshProxyFailed, ErrRegistration, ErrSSLProxy, ErrSealApprovalContractHashMismatch, ErrSealApprovalExpired, ErrSealApprovalLocationMismatch, ErrSealApprovalNonceUsed, ErrSealApprovalNotApproved, ErrSealApprovalSealMismatch, ErrSealApprovalTransactorMismatch, ErrSealUseAlreadyConsumed, ErrStateMismatch, ErrTimeout, ErrTokenExchange, ErrTokenExpired, EventAuthURL, EventComplete, EventError, FileTokenStore, FilterStatusAdminBypass, FilterStatusDisabledByFlag, FilterStatusFallbackMissingUser, FilterStatusFallbackNoBuckets, FilterStatusFallbackTkdistError, FilterStatusFallbackTkdistSkew, FilterStatusInternalBypass, FilterStatusOK, FilterStatusUnknown, HTTPError, IdempotencyKeyHeader, InMemoryTokenStore, LocalStorageTokenStore, ModelNotFoundError, NetworkError, OAuthTokenEndpointError, OpenAIAdapter, OrderTerminalError, ProductFamilyEnum, ProviderFormat, RETRY_ADVICE_REASONS, RateLimitError, RegionScopeEnum, ScopeAI, ScopeAccount, ScopeAgentAccessManage, ScopeChatBridge, ScopeChatBridgeRead, ScopeChatBridgeRotate, ScopeChatBridgeWrite, ScopeComplianceContractSigningRead, ScopeComplianceContractSigningWrite, ScopeComplianceContractTemplateRead, ScopeComplianceContractTemplateWrite, ScopeComplianceEvidenceRead, ScopeComplianceEvidenceWrite, ScopeComplianceReportsPublish, ScopeComplianceReportsRead, ScopeComplianceReportsWrite, ScopeComplianceSealApprovalApprove, ScopeComplianceSealApprovalRequest, ScopeComplianceSealManage, ScopeComplianceSealUseExecute, ScopeComplianceTimestampIssue, ScopeComplianceTimestampVerify, ScopeEntitlements, ScopeModels, ScopeModelsChat, ScopeProfile, ScopeRemoteControl, ScopeRemoteControlAgentRun, ScopeRemoteControlPermissionResponse, ScopeRemoteControlSessionControl, ScopeSkillStore, ScopeSkills, ScopeTokenPackages, ScopeTools, ScopeToolsExecute, ScopeWallet, ScopeWalletReadonly, ServerToolTypeWebSearch, StreamError, ThinkingHigh, ThinkingHighMinMaxTokens, ThinkingMax, ThinkingMaxFallbackMaxTokens, ThinkingOff, agentAccessScopes, allScopes, anthropicResponseTextContent, anthropicResponseThinkingContent, anthropicResponseToolUseBlocks, apiResponseBusinessError, apiResponseGetMessage, asCredentialRef, authorize, bucketInfoIsCommercial, bucketRowIsCommercial, buildBetas, chatBridgeScopes, classifyComplianceError, classifySourcesEvent, commerceScopes, completeWebAuthorizationRequest, complianceErrorToRetryAdvice, complianceScopes, computeBackoff, createWebAuthorizationRequest, defaultRetryable, defaultSafeToRetry, discover, discoverWebOAuthMetadata, discoverWithProfile, effectivePolicy, exchangeCode, extractAnthropicBlockMeta, fileLockDefaults, findDesktopVisualUnderstandingModel, findFirstModelByInputModality, generateState, getAdapter, getAdapterForModel, getWindowLimitStreamDetails, isBillingConfirmable, isChannelInboundEvent, isComplianceBusinessError, isComplianceTerminalError, isContinuableWindowLimitError, isIntegrationStatus, isInvalidGrantError, isPlatform, isRegion, isSSECommentLine, isSSLError, isTerminalRemoteEvent, isValidTokenSet, isWindowLimitError, isWindowLimitStreamError, maxEndUserIdLength, modelScopes, modelSupportsImageInput, modelSupportsInputModality, newFileTokenStore, newThinkingConfig, newTokenSet, newWebSearchTool, normalizeGatewayBaseURL, normalizeOverrideBaseURL, parseNotificationEvent, parseRemoteControlEvent, parseSettlement, parseSourcesEvent, refreshToken, register, registerWebOAuthClient, remoteControlScopes, retryReasonForComplianceKey, retryReasonForOAuthError, revokeToken, sanitize_exports as sanitize, skillScopes, tokenSetIsExpired, uniqueMerge, validateEndUserId };
7732
7816
  //# sourceMappingURL=index.mjs.map
7733
7817
  //# sourceMappingURL=index.mjs.map