dsh-bailinghub 0.6.0 → 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.
- package/CHANGELOG.md +5 -0
- package/README.md +6 -6
- package/docs/COMPATIBILITY.md +7 -7
- package/docs/README.zh-CN.md +6 -6
- package/docs/RELEASE_NOTES_v0.7.0.en.md +15 -0
- package/docs/RELEASE_NOTES_v0.7.0.md +15 -0
- package/docs/UPGRADE_v0.7.0.en.md +15 -0
- package/docs/UPGRADE_v0.7.0.md +15 -0
- package/docs/USAGE_SERVICE.md +96 -0
- package/lib/index.js +9 -0
- package/lib/invocation-journal.js +5 -0
- package/lib/model-gateway-transport.js +246 -0
- package/lib/runtime.js +24 -0
- package/lib/session-usage-store.js +280 -0
- package/lib/usage-llm-stream.js +157 -0
- package/lib/usage-model-transport.js +206 -0
- package/lib/usage-provider-stream.js +82 -0
- package/package.json +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 - 2026-09-23
|
|
4
|
+
|
|
5
|
+
Optional host-orchestrated model gateway, USD plan presentation and asynchronous image tools.
|
|
6
|
+
See [changes](docs/RELEASE_NOTES_v0.7.0.en.md) and [upgrade](docs/UPGRADE_v0.7.0.en.md).
|
|
7
|
+
|
|
3
8
|
## 0.6.0 - 2026-09-16
|
|
4
9
|
|
|
5
10
|
See [scenarios and upgrade](docs/RELEASE_NOTES_v0.6.0.md) · [English](docs/RELEASE_NOTES_v0.6.0.en.md).
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# BailingHub for DeepSeek Harness
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.7.0: optional model services and plan billing
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Keep orchestration local while Core relays model requests and meters shared allowance asynchronously. Discover chat models and image tools separately; preserve business authorization, approvals and audit. Pair Core 0.9.0 / SDK 0.7.0 / DSH 0.7.0.
|
|
6
6
|
|
|
7
|
-
[Changes](docs/RELEASE_NOTES_v0.
|
|
7
|
+
[Changes](docs/RELEASE_NOTES_v0.7.0.en.md) · [Upgrade](docs/UPGRADE_v0.7.0.en.md)
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
[简体中文](docs/README.zh-CN.md) | English
|
|
@@ -40,14 +40,14 @@ recommended by DeepSeek.
|
|
|
40
40
|
|
|
41
41
|
You need Node.js `22.19.0+` or `24+`, pnpm, and a compatible DeepSeek Harness release. Your
|
|
42
42
|
administrator must first connect the business system to BailingHub. The matched release set is
|
|
43
|
-
**BailingHub Core 0.
|
|
43
|
+
**BailingHub Core 0.9.0 → BailingHub MCP/SDK 0.7.0 → this plugin 0.7.0**.
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
npm install --global pnpm @deepseek-ai/dsh@0.1.1-rc.2
|
|
47
|
-
dsh plugin --profile web add dsh-bailinghub@0.
|
|
47
|
+
dsh plugin --profile web add dsh-bailinghub@0.7.0
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
The plugin installs its exact `bailinghub-mcp-server@0.
|
|
50
|
+
The plugin installs its exact `bailinghub-mcp-server@0.7.0` dependency automatically.
|
|
51
51
|
For an existing installation, read the [migration steps from 0.4.0 and earlier](docs/MIGRATION_VNEXT.md).
|
|
52
52
|
|
|
53
53
|
Follow the [getting started guide](docs/GETTING_STARTED.md) to enter your administrator's four
|
package/docs/COMPATIBILITY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Current 0.
|
|
1
|
+
# Current 0.7.0 pairing
|
|
2
2
|
|
|
3
|
-
Use Core 0.
|
|
3
|
+
Use Core 0.9.0, SDK 0.7.0 and DSH 0.7.0 for optional model plans and image tools. Existing business-governance APIs retain their documented minima. Host orchestration, scope, approval and audit rules remain in effect. See [upgrade](UPGRADE_v0.7.0.en.md).
|
|
4
4
|
|
|
5
5
|
# Compatibility
|
|
6
6
|
|
|
@@ -10,23 +10,23 @@ The additive [capability feedback contract](CAPABILITY_FEEDBACK.md) requires the
|
|
|
10
10
|
Core 0.8.0 / SDK 0.6.0 / DSH 0.6.0 release set for complete counts and error detail. Older unenrolled combinations keep their existing business behavior with unknown optional metadata.
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
## Native Agent Client 0.
|
|
13
|
+
## Native Agent Client 0.7.0
|
|
14
14
|
|
|
15
15
|
| Component | Release pairing / requirement |
|
|
16
16
|
| --- | --- |
|
|
17
17
|
| DeepSeek Harness | `0.1.1-rc.2`; real Session and native Cordis lifecycle |
|
|
18
18
|
| Node.js | `^22.19.0` or `>=24.0.0` |
|
|
19
19
|
| DSH tool presentation | Native Tool Mode; Code Mode deliberately degraded |
|
|
20
|
-
| Generic Agent Client SDK | Exact `bailinghub-mcp-server@0.
|
|
21
|
-
| BailingHub Core | `bailinghub@0.
|
|
20
|
+
| Generic Agent Client SDK | Exact `bailinghub-mcp-server@0.7.0` via `./sdk` |
|
|
21
|
+
| BailingHub Core | `bailinghub@0.9.0`, with outstanding migrations through 064 applied |
|
|
22
22
|
| Selected scope | Single account, same-system multiple accounts, or different Client Apps/workspaces on one Hub and audit domain |
|
|
23
23
|
| Original authorization | A distinct original Agent Session for every selected target |
|
|
24
24
|
| Persistence | Existing same-system v1 scope/outbox and cross-system v2 records |
|
|
25
25
|
|
|
26
|
-
Install `dsh-bailinghub@0.
|
|
26
|
+
Install `dsh-bailinghub@0.7.0`; its ordinary dependency installs the exact SDK automatically.
|
|
27
27
|
Core 0.6.1 and SDK/plugin 0.4.0 remain the historical same-system baseline, not an alternative
|
|
28
28
|
pairing for new cross-system features. See the [upgrade steps](MIGRATION_VNEXT.md) and
|
|
29
|
-
[release
|
|
29
|
+
[release scenarios](RELEASE_NOTES_v0.7.0.en.md).
|
|
30
30
|
|
|
31
31
|
### Authorization subject display
|
|
32
32
|
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# BailingHub for DeepSeek Harness
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.7.0:可选模型服务与套餐计费
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
本地宿主继续编排,中枢提供模型转发、共享额度与异步计量。对话模型和图片工具分开下发;原业务权限、审批与审计保持。配套 Core 0.9.0 / SDK 0.7.0 / DSH 0.7.0。
|
|
6
6
|
|
|
7
|
-
[本次变化](RELEASE_NOTES_v0.
|
|
7
|
+
[本次变化](RELEASE_NOTES_v0.7.0.md) · [升级指南](UPGRADE_v0.7.0.md)
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
[English](../README.md) | 简体中文
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
## 安装与开始使用
|
|
36
36
|
|
|
37
37
|
需要 Node.js `22.19.0+` 或 `24+`、pnpm,以及兼容的 DeepSeek Harness。管理员应先完成业务系统
|
|
38
|
-
接入。配套版本为 **BailingHub Core 0.
|
|
38
|
+
接入。配套版本为 **BailingHub Core 0.9.0 → BailingHub MCP/SDK 0.7.0 → 本插件 0.7.0**。
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
41
|
npm install --global pnpm @deepseek-ai/dsh@0.1.1-rc.2
|
|
42
|
-
dsh plugin --profile web add dsh-bailinghub@0.
|
|
42
|
+
dsh plugin --profile web add dsh-bailinghub@0.7.0
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
插件会自动安装精确依赖 `bailinghub-mcp-server@0.
|
|
45
|
+
插件会自动安装精确依赖 `bailinghub-mcp-server@0.7.0`,无需另装 SDK。
|
|
46
46
|
已经使用旧版的用户请先看[从 0.4.0 及更早版本升级的步骤](MIGRATION_VNEXT.md)。
|
|
47
47
|
|
|
48
48
|
按照[开始使用指南](GETTING_STARTED.zh-CN.md)填写管理员提供的四项公开连接信息,再到浏览器授权。
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# DSH 0.7.0: optional model services and shared plan billing
|
|
2
|
+
|
|
3
|
+
Release pairing: Core 0.9.0, SDK 0.7.0 and DSH 0.7.0. The changes and integration impact below are relative to public 0.6.0.
|
|
4
|
+
|
|
5
|
+
Local agents can use administrator-configured model plans and discover image generation as a model tool. Context, tool loops, subtasks and business orchestration stay in the host. Core relays model requests, checks allowance and meters usage asynchronously.
|
|
6
|
+
|
|
7
|
+
- Separate `modelModels` and `modelTools` catalogs; bind requests to exact service IDs.
|
|
8
|
+
- Preserve provider streaming and complete tool declarations through `stream`. Settlement does not hold usable results or impose user-turn/tool-loop limits.
|
|
9
|
+
- Validate explicit `periodAllowanceUsd`; display server `presentation` as credits or percentage. Provider Token counts remain usage details, not a quota denominator.
|
|
10
|
+
- Submit asynchronous `runModelTool` once and use `recoverOperation` with its original operation ID after ACK loss or restart. Distinguish pending, failed, uncertain and pending billing outcomes.
|
|
11
|
+
- Preserve original identity, business authorization, approval, task, artifact and audit boundaries. Generating an image does not upload it to a business system.
|
|
12
|
+
|
|
13
|
+
Executable image support follows the Core catalog. Video/audio declarations do not promise execution. Reference prices are not supplier invoices. Payments, orders and currency conversion belong to the product backend.
|
|
14
|
+
|
|
15
|
+
See the [integration contract](USAGE_SERVICE.md) and [upgrade guide](UPGRADE_v0.7.0.en.md).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# DSH 0.7.0:可选模型服务与统一套餐计费
|
|
2
|
+
|
|
3
|
+
版本配套:Core 0.9.0、SDK 0.7.0、DSH 0.7.0。以下为相对公开 0.6.0 的新增能力与接入影响。
|
|
4
|
+
|
|
5
|
+
本地智能体可使用管理员配置的模型套餐,并将图片生成作为按需工具调用。上下文、工具循环、子任务和业务编排仍由本地宿主执行;中枢负责模型转发、额度检查及异步计量。
|
|
6
|
+
|
|
7
|
+
- `modelModels` 与 `modelTools` 分离对话模型和可调用模型工具;使用服务标识绑定所选模型,不按名称猜测。
|
|
8
|
+
- `stream` 保留供应商流式内容与工具声明,结算不阻塞可用结果;不增加用户轮次或工具循环上限。
|
|
9
|
+
- `modelSummary` 严格读取周期额度 `periodAllowanceUsd`,用户展示继续消费服务端 `presentation`(积分或百分比),原始 Token 仅供用量明细。
|
|
10
|
+
- `runModelTool` 异步受理生成;`recoverOperation` 按原 operation ID 查询,回包丢失不新建生成请求。明确失败、处理中、结果不确定和待计量分开处理。
|
|
11
|
+
- 原业务授权、审批、任务控制、附件上传与审计约束保持;套餐不扩大业务权限。图片结果与上传到业务系统是两个独立动作。
|
|
12
|
+
|
|
13
|
+
图片可执行适配以 Core 工具目录为准;视频与语音声明不等于已支持执行。参考价格不等于供应商实际账单。支付、订单、汇率由业务侧负责。
|
|
14
|
+
|
|
15
|
+
参见[接入契约](USAGE_SERVICE.md)及[升级指南](UPGRADE_v0.7.0.md)。
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Upgrade to DSH 0.7.0
|
|
2
|
+
|
|
3
|
+
Upgrade with the matched Core 0.9.0 / SDK 0.7.0 / DSH 0.7.0 release set. Existing public 0.6.0 business-governance integrations remain supported; managed model plans require explicit host integration.
|
|
4
|
+
|
|
5
|
+
1. Back up and apply the Core 0.9.0 upgrade migrations. Enabling this optional module does not recreate business authorizations.
|
|
6
|
+
2. Install exact paired packages. DSH 0.7.0 depends on SDK 0.7.0. Restart the host and verify its actual Profile resolution.
|
|
7
|
+
3. Exchange a Usage credential through the trusted backend, preserving plan-wide or single-service scope. Provider keys stay in Core. Read capabilities, modelModels, modelTools and modelSummary; never substitute a default model or authorization for an empty catalog.
|
|
8
|
+
4. Keep orchestration local, provide complete tool schemas, use transport.stream for plan models and runModelTool for generation. Persist the original operation ID before dispatch. Late responses cannot reactivate cancelled/ended turns or override storage failures.
|
|
9
|
+
5. Verify streaming, image results, shared allowance and original-request recovery. A complete result is usable while billing is pending; do not reissue it or block unrelated new user messages for settlement.
|
|
10
|
+
|
|
11
|
+
Display server presentation. Periodic allowance is grant.periodAllowanceUsd, not sale price, token totals or model count. Refresh summary after an administrator resets allowance; do not recreate accounts or credentials.
|
|
12
|
+
|
|
13
|
+
Model billing is new relative to public 0.6.0 and provides no legacy Token-quota aliases. No history or account purge is required. Upgrading public Core 0.8.0 only adds outstanding 063/064 migrations; maintaining test configuration is not a real-customer upgrade step. Existing business Sessions and original invocation recovery remain unchanged.
|
|
14
|
+
|
|
15
|
+
Respect callable=false for unsupported video/audio adapters. Inspect uncertain requests by original ID only. See the [contract](USAGE_SERVICE.md).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# DSH 0.7.0 升级
|
|
2
|
+
|
|
3
|
+
使用 Core 0.9.0 / SDK 0.7.0 / DSH 0.7.0 配套升级。公开 0.6.0 的业务治理接入继续保持,可选模型套餐需宿主显式接入。
|
|
4
|
+
|
|
5
|
+
1. 按 Core 0.9.0 升级指南完成备份和新增迁移。未启用套餐时不需要重建原业务授权。
|
|
6
|
+
2. 安装精确配套包;DSH 0.7.0 固定依赖 SDK 0.7.0。重启宿主并核对实际 Profile 加载的版本,不能只检查开发目录。
|
|
7
|
+
3. 可信业务后端签发使用凭据,明确套餐范围或单模型范围;供应商密钥保留在 Core。读取 capabilities、modelModels、modelTools、modelSummary。无可用项时显示配置原因,不回退到其他模型或授权。
|
|
8
|
+
4. 宿主继续本地编排,将完整工具 schema 提供给模型;套餐使用 `stream`,生成工具使用 `runModelTool`。持久保存原 operation ID,取消/存储失败/已结束轮次不能被迟到结果重新启用。
|
|
9
|
+
5. 核对文本流式、图片结果、同一套餐额度及原请求恢复。`result_state=complete` 即可使用结果,`billing_state=pending` 不要求重复请求或阻止下一条独立用户消息。
|
|
10
|
+
|
|
11
|
+
额度显示直接读取 presentation;周期剩余按原 grant 的 periodAllowanceUsd,而非售价、Token 数或模型数量计算。后台手动重置后刷新摘要即可,无需重建账户或凭据。
|
|
12
|
+
|
|
13
|
+
模型计费是相对公开 0.6.0 的新增模块,不提供旧 Token 额度接口别名,也不要求清退历史或账户。从公开 Core 0.8.0 升级仅应用新增 063/064 迁移;测试配置的维护不属于真实客户升级步骤。已有业务 Session 和原 invocation 恢复规则不变。
|
|
14
|
+
|
|
15
|
+
视频/语音不含可执行适配时必须尊重 callable=false。断网或不确定请求只按原 ID 核对,不重新派发生成。详见[契约](USAGE_SERVICE.md)。
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# USD model gateway for local agents
|
|
2
|
+
|
|
3
|
+
A plan has a USD allowance, one multiplier, and a live selection of conversation models and model tools. The Hub relays individual requests and meters their cost. The host keeps its original planning, tool selection and AgentLoop. Business authorization, approval, session scope, task controls and conversation archives remain separate.
|
|
4
|
+
|
|
5
|
+
This optional module pairs Core 0.9.0, SDK 0.7.0 and DSH 0.7.0. It is new relative to public SDK/DSH 0.6.0 and has no legacy Token-allowance aliases, silent billing fallback, balance conversion or automatic BYOK substitution. Public Core 0.8.0 upgrades add outstanding 063/064 migrations and retain existing data. Do not erase business authorizations, history or unrelated sessions; abandoned test configuration is separate maintenance, not a customer upgrade requirement.
|
|
6
|
+
|
|
7
|
+
## Identity and catalog
|
|
8
|
+
|
|
9
|
+
The trusted product backend issues a Usage credential for the original Hub/user/account. `model_access:'token_gateway'` remains the credential scope spelling for the whole permitted plan; it does not select a Token billing algorithm. A deliberately single-service credential stays restricted to its original service. Supplier URLs and API keys stay server-side.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { createUsageClient } from 'bailinghub-mcp-server/usage'
|
|
13
|
+
const client = createUsageClient({
|
|
14
|
+
hubUrl: trustedLogin.hubUrl,
|
|
15
|
+
userId: trustedLogin.userId,
|
|
16
|
+
accountId: trustedLogin.accountId,
|
|
17
|
+
serviceId: trustedLogin.serviceId,
|
|
18
|
+
accessTokenProvider: () => secureCredentials.readUsageToken(),
|
|
19
|
+
})
|
|
20
|
+
await client.capabilities()
|
|
21
|
+
const models = await client.modelModels()
|
|
22
|
+
const tools = await client.modelTools()
|
|
23
|
+
const summary = await client.modelSummary()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Capabilities must advertise `model_gateway` with schema `bailing.model-gateway.v1`, `billing_unit:'USD'`, `orchestration:'host'`, `turn_required:false`, streaming, native provider response envelopes and asynchronous settlement. Old Core/SDK returns explicit unsupported, not an alternative billing path.
|
|
27
|
+
|
|
28
|
+
`modelModels()` reads `/usage/v1/model/models`, schema `bailing.model-models.v1`. Its `items` are only conversation models. Render the operator's `label`, store the immutable `service_id`, never route by label or provider model name. Every read refreshes current plan membership. An empty directory is valid and does not authorize choosing an unrelated model.
|
|
29
|
+
|
|
30
|
+
`modelTools()` reads `/usage/v1/model/tools`, schema `bailing.model-tools.v1`. Each item has a stable `service_id`, `service_revision`, display label, capability/output kinds and a `tool:{name,description,input_schema}`. Register only `callable:true` items in the host's local tool registry. Keep unavailable items as configuration diagnostics with their explicit `reason`. Catalog reads neither generate media nor start business runs. Namespace registrations by service identity to avoid duplicate operator-defined names or collisions with business tools; tool names alone are never authorization identities.
|
|
31
|
+
|
|
32
|
+
## Shared allowance and display
|
|
33
|
+
|
|
34
|
+
`modelSummary()` returns `bailing.billing-summary.v1`, with USD fields `availableUsd`, `consumedUsd`, `currentPeriodConsumedUsd`, `overageUsd`, the original grant and current plan. The plan supplies `serviceIds` and one `multiplier`; per-model input/output multipliers and Token balances no longer exist.
|
|
35
|
+
|
|
36
|
+
Use `presentation` (`bailing.usage-presentation.v1`) for customer balances: allowance packs show credits; periodic plans show remaining percentage and reset time. Never recalculate balances locally from raw tokens, prices or a previous model response. Missing/malformed presentation is `USAGE_RESPONSE_INVALID`, not a guessed balance. Account details may show reported provider Token counts as usage information, separate from allowance and percentage. If unavailable, show not provided, never fabricate zero Tokens.
|
|
37
|
+
|
|
38
|
+
Reference provider pricing is not an actual supplier invoice or guaranteed margin. Each request snapshots its plan multiplier and reference price; server-side USD accounting is exact to twelve decimal places. Receipt fields `reference_cost_usd`, `billed_usd`, `overage_usd` are amounts; `usage` contains integer Token counts when reported, and `raw_usage` can contain non-Token measurements. Unknown usage keeps billing pending. No client-side multiplication, second debit or extra model request is allowed.
|
|
39
|
+
|
|
40
|
+
## Request and recovery API
|
|
41
|
+
|
|
42
|
+
| SDK method | Behavior |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `modelComplete(input)` | One JSON model request, no server agent loop |
|
|
45
|
+
| `modelStream(input)` | Provider packets followed by the original durable operation receipt |
|
|
46
|
+
| `runModelTool(input)` | One model-tool generation request |
|
|
47
|
+
| `inspectModelRequest(operationId, originalCoordinates)` | Read original operation only |
|
|
48
|
+
| `cancelModelRequest(operationId, originalCoordinates)` | Cancel original operation, never replay it |
|
|
49
|
+
|
|
50
|
+
Conversation requests use `/usage/v1/model/requests` and `/usage/v1/model/requests/stream`. Tool requests use `POST /usage/v1/model/tools/requests` with `{operation_id,service_id,arguments,conversation_id?,turn_id?}`. A tool POST can return HTTP 202 with a durably admitted pending operation. Poll the original request until complete or cancelled; do not submit another generation. All use the same original request inspection/cancellation path and `bailing.model-operation.v1` receipt. Streaming frames use `bailing.model-stream.v1`.
|
|
51
|
+
|
|
52
|
+
A completed model result can have `billing_state:'pending'`; billing settlement must not stop the local loop. Native provider response bytes are preserved in `bailing.provider-response.v1`. The local provider adapter interprets content, finish reason and tool calls. A tool result uses `bailing.model-tool-result.v1` with typed outputs. Downloading or using a result in a business attachment space is an explicit subsequent host action, never an automatic business write by the Hub.
|
|
53
|
+
|
|
54
|
+
Persist the original operation ID, selected service and local event coordinates before any dispatch. If a POST loses its ACK, inspect that original ID; do not create a new ID, regenerate an image, change service, retry through BYOK or label the operation not dispatched. Read/cancel failures preserve uncertainty. Storage failure and recovery gaps remain primary errors.
|
|
55
|
+
|
|
56
|
+
## Host integration
|
|
57
|
+
|
|
58
|
+
The host must actually register executable model tools; putting their description in a prompt alone is insufficient. Present concise guidance alongside current registered tools: “These are the model tools available under this plan. Use the supplied schemas. When an operation is pending or its result is uncertain, recover the original operation ID rather than repeating generation.”
|
|
59
|
+
|
|
60
|
+
The server still rechecks the live plan and selected service on each new request. A cached descriptor is not a permission grant. Business tool scope and approvals are not bypassed by generating a media file.
|
|
61
|
+
|
|
62
|
+
## Durable DSH host adapter
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { createUsageModelTransport, usageNativeModelChunks } from 'dsh-bailinghub'
|
|
66
|
+
const usage = createUsageModelTransport({
|
|
67
|
+
client,
|
|
68
|
+
ensureSessionPersisted: session => persistRealSessionAndConfirm(session),
|
|
69
|
+
getPrimaryStatus: session => currentPersistenceStatus(session),
|
|
70
|
+
})
|
|
71
|
+
const tools = await usage.modelTools()
|
|
72
|
+
const receipt = await usage.runModelTool(realSession, {
|
|
73
|
+
userMessageId: originalUserMessageId,
|
|
74
|
+
modelRequestId: stableLocalToolCallId,
|
|
75
|
+
serviceId: selectedTool.service_id,
|
|
76
|
+
arguments: validatedToolArguments,
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Register each callable descriptor using the native host tool registry and route its stable service ID to `usage.runModelTool`. Reuse the same local tool call ID when inspecting an unresolved call. `recoverOperation(session, operationId)` reads the saved original result; it never dispatches a second generation. For a new intentional generation, use a new local call ID.
|
|
81
|
+
|
|
82
|
+
`model({serviceId,refresh?})`, `complete(session,input)`, `stream(session,input)`, `status(session)`, `endTurn`, `cancelTurn`, `completeRequest` and `streamRequest` remain the host transport interfaces. Omit `mode` or use `model_gateway`. The descriptor cache lasts 60 seconds; cache hits do not extend freshness. Use `refresh:true` after settings change. No message-count, paid-turn count or task wall-clock quota is added.
|
|
83
|
+
|
|
84
|
+
The real durable DSH Session indexes `bailinghub/model-request` markers. Each request has a bounded CAS sidecar containing identity/hash/state only, not messages, tool arguments, provider credentials or generated content. Save both before dispatch. Retain them for same-operation recovery; missing metadata produces `USAGE_RECOVERY_GAP`, never an invented request. Old unpublished Token billing markers are not imported into the new ledger; ordinary business session history remains intact.
|
|
85
|
+
|
|
86
|
+
Native streaming uses `usageNativeModelChunks` with the host's actual provider parser. Text/reasoning may display incrementally, but executable tool blocks and finish wait for the original durable receipt. Ending/cancelling the user input immediately fences late results; they remain inspectable history and do not restart tools. Final local save failures remain `storage_error` even when the provider result is known. Unknown billing settlement alone does not make a complete model result unusable.
|
|
87
|
+
|
|
88
|
+
Synthetic acceptance covers current account catalog isolation, empty/unavailable tools, generation ACK loss across full store reopen, cancellation before late generation, raw Token usage alongside USD amounts, native provider streaming, original request recovery and long local tasks.
|
|
89
|
+
|
|
90
|
+
### Image provider outcome receipts
|
|
91
|
+
|
|
92
|
+
`result_state=failed`, `state=failed`, `dispatch=rejected` is a confirmed provider rejection. Stop waiting; show the bounded `error` and `next_action=contact_operator`. Never automatically create a replacement operation. `unknown` is not a running task or proof of zero cost: preserve the original ID for inspection. Only `pending` means keep waiting. A complete result is usable independently of pending billing.
|
|
93
|
+
|
|
94
|
+
The diagnostic whitelist is `code`, fixed safe `message`, optional `http_status`, `provider_code`, `provider_request_id`, `retryable=false`, and `next_action`. Provider bodies and credentials are excluded. Cancelled/ended turns never regain active tools through late receipts. Existing durable request identities and prior unknown records remain unchanged.
|
|
95
|
+
|
|
96
|
+
DSH stores failed requests using the existing local `rejected` state and returns `delivery_state=request_failed`. Unknown receipts return `unresolved_original`; pending receipts retain `pending_original`. No Session event or sidecar migration is required, and primary storage/recovery errors retain priority.
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Schema from '@deepseek-ai/schemastery'
|
|
2
2
|
|
|
3
3
|
import { BailingHubAgentClientRuntime } from './runtime.js'
|
|
4
|
+
import { createUsageModelTransport } from './usage-model-transport.js'
|
|
4
5
|
import { createTransportProvider } from './transport.js'
|
|
5
6
|
|
|
6
7
|
export const name = 'dsh-bailinghub'
|
|
@@ -36,6 +37,8 @@ export function createAgentClientPlugin(options = {}) {
|
|
|
36
37
|
{ scopeStore: options.scopeStore, archiveStore: options.archiveStore, artifactSource: options.artifactSource, artifactStore: options.artifactStore, invocationStore: options.invocationStore, taskStore: options.taskStore, toolLifecycle: options.toolLifecycle },
|
|
37
38
|
).install()
|
|
38
39
|
ctx.provide('bailingHubAgentClient', runtime)
|
|
40
|
+
// Explicit host integration only; installing business tools does not intercept model traffic.
|
|
41
|
+
if (options.usage) ctx.provide('bailingHubUsage', createUsageModelTransport(options.usage))
|
|
39
42
|
},
|
|
40
43
|
}
|
|
41
44
|
}
|
|
@@ -60,3 +63,9 @@ export { createFileArtifactStore, createMemoryArtifactStore } from './artifact-s
|
|
|
60
63
|
export { createFileInvocationStore, createMemoryInvocationStore } from './invocation-store.js'
|
|
61
64
|
|
|
62
65
|
export { createFileSessionTaskStore, createMemorySessionTaskStore } from './session-task-store.js'
|
|
66
|
+
|
|
67
|
+
export { BailingHubUsageModelTransport, createUsageModelTransport } from './usage-model-transport.js'
|
|
68
|
+
export { usageLlmChunks } from './usage-llm-stream.js'
|
|
69
|
+
export { createFileSessionUsageStore, createMemorySessionUsageStore } from './session-usage-store.js'
|
|
70
|
+
|
|
71
|
+
export { usageNativeModelChunks } from './usage-provider-stream.js'
|
|
@@ -92,6 +92,11 @@ export class InvocationJournal {
|
|
|
92
92
|
return result
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Wait only for local work already queued. Do not read, retry or restore records.
|
|
96
|
+
async drain(sessionId) {
|
|
97
|
+
await this.queues.get(sessionId)
|
|
98
|
+
}
|
|
99
|
+
|
|
95
100
|
normalizeFailure(error, id) {
|
|
96
101
|
return error?.feedback ? error : recoveryError(error?.code === 'INVOCATION_STORE_CONFLICT'
|
|
97
102
|
? 'invocation_store_conflict' : 'invocation_store_unavailable', id)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
export const MODEL_REQUEST_EVENT = 'bailinghub/model-request'
|
|
4
|
+
const CLOSED_EVENT = 'bailinghub/model-request-group-ended'
|
|
5
|
+
const SCHEMA = 'bailing.usage-session.v1'
|
|
6
|
+
const META_SCHEMA = 'bailing.model-request.v1'
|
|
7
|
+
const REQUIRED = ['modelModels', 'modelSummary', 'modelComplete', 'modelStream', 'inspectModelRequest', 'cancelModelRequest']
|
|
8
|
+
const clone = value => structuredClone(value)
|
|
9
|
+
|
|
10
|
+
/** Request-sized sidecars prevent a long task from consuming a session-wide metadata budget.
|
|
11
|
+
* The real durable Session is the original request index. No transcript or credential is copied.
|
|
12
|
+
*/
|
|
13
|
+
export class ModelGatewayTransport {
|
|
14
|
+
constructor(host, helpers) { this.host = host; Object.assign(this, helpers) }
|
|
15
|
+
assertSupported() {
|
|
16
|
+
if (REQUIRED.some(name => typeof this.host.client?.[name] !== 'function')) throw this.failure('USAGE_UNSUPPORTED')
|
|
17
|
+
}
|
|
18
|
+
markers(session) { return session.events.filter(e => e.type === MODEL_REQUEST_EVENT) }
|
|
19
|
+
locate(session, id) {
|
|
20
|
+
const found = this.markers(session).filter(e => e.data?.operationId === id)
|
|
21
|
+
if (found.length !== 1) throw this.failure('USAGE_RECOVERY_GAP')
|
|
22
|
+
return found[0].data
|
|
23
|
+
}
|
|
24
|
+
key(session, id) { return `model:${this.hash(session.id)}:${id}` }
|
|
25
|
+
async load(session, meta) {
|
|
26
|
+
const owner = this.eventFor(session, meta.turnId)
|
|
27
|
+
if (meta.schema !== META_SCHEMA || meta.conversationId !== session.id || meta.eventSeq !== owner.seq
|
|
28
|
+
|| meta.eventHash !== this.hash(owner.data) || this.hash(meta.binding) !== this.hash(this.host.assertSupported())) throw this.failure('USAGE_RECOVERY_GAP')
|
|
29
|
+
let record
|
|
30
|
+
try { record = await this.host.store.load(this.key(session, meta.operationId)) } catch { throw this.failure('USAGE_STORAGE_ERROR') }
|
|
31
|
+
if (!record || record.schema !== SCHEMA || record.entries?.length !== 1
|
|
32
|
+
|| this.hash(record.entries[0].request?.metadata) !== this.hash(meta)) throw this.failure('USAGE_RECOVERY_GAP')
|
|
33
|
+
return record
|
|
34
|
+
}
|
|
35
|
+
async save(session, record) {
|
|
36
|
+
const previous = record.revision; record.revision++
|
|
37
|
+
try { await this.host.store.save(record.sessionId, clone(record), previous || null) }
|
|
38
|
+
catch { this.host.storageFailures.add(session.id); throw this.failure('USAGE_STORAGE_ERROR') }
|
|
39
|
+
}
|
|
40
|
+
closed(session, meta) {
|
|
41
|
+
return this.host.cancelled.has(`${session.id}:${meta.turnId}`) || this.host.ended.has(`${session.id}:${meta.turnId}`)
|
|
42
|
+
|| session.events.some(e => e.type === CLOSED_EVENT && e.data?.id === meta.turnId)
|
|
43
|
+
|| !this.isLiveMessage(session, this.eventFor(session, meta.turnId))
|
|
44
|
+
}
|
|
45
|
+
async model(options = {}) {
|
|
46
|
+
this.assertSupported()
|
|
47
|
+
const key = options.serviceId ?? this.host.client.binding.serviceId
|
|
48
|
+
const cached = this.host.modelModelsCache
|
|
49
|
+
const hit = !options.refresh && cached?.until > Date.now()
|
|
50
|
+
const models = hit ? cached.value : await this.host.client.modelModels(options)
|
|
51
|
+
// Cache hits do not extend the freshness deadline; live plan edits must become visible.
|
|
52
|
+
if (!hit) this.host.modelModelsCache = { value: clone(models), until: Date.now() + 60_000 }
|
|
53
|
+
const model = models.items.find(item => item.service_id === key)
|
|
54
|
+
if (!model) throw this.failure('SERVICE_NOT_ENTITLED', { feedback: { schema: 'bailing.usage-feedback.v1', code: 'SERVICE_NOT_ENTITLED', dispatch: 'not_dispatched', next_action: 'select_model', retryable: false } })
|
|
55
|
+
return clone(model)
|
|
56
|
+
}
|
|
57
|
+
async complete(session, input, options = {}, modelTool = false) {
|
|
58
|
+
this.assertSupported()
|
|
59
|
+
let copied
|
|
60
|
+
try { copied = clone(input) } catch { throw this.failure('USAGE_INPUT_INVALID') }
|
|
61
|
+
if (!copied || typeof copied !== 'object' || Array.isArray(copied)
|
|
62
|
+
|| Object.keys(copied).some(key => !['userMessageId', 'modelRequestId', 'serviceId', 'requestGroupId', 'requestKind',
|
|
63
|
+
...(modelTool ? ['arguments'] : ['messages', 'tools', 'tool_choice', 'temperature', 'provider_options'])].includes(key))) throw this.failure('USAGE_INPUT_INVALID')
|
|
64
|
+
const prepared = await this.host.serial(session, async () => {
|
|
65
|
+
await this.host.durable(session)
|
|
66
|
+
const binding = this.host.assertSupported()
|
|
67
|
+
if (this.markers(session).some(marker => this.hash(marker.data?.binding) !== this.hash(binding))) throw this.failure('USAGE_BINDING_MISMATCH')
|
|
68
|
+
const event = this.eventFor(session, copied.userMessageId), requestKey = this.exact(copied.modelRequestId)
|
|
69
|
+
const serviceId = this.exact(copied.serviceId ?? this.host.client.binding.serviceId)
|
|
70
|
+
const request = Object.fromEntries((modelTool ? ['arguments'] : ['messages', 'tools', 'tool_choice', 'temperature', 'provider_options'])
|
|
71
|
+
.filter(key => copied[key] !== undefined).map(key => [key, copied[key]]))
|
|
72
|
+
if (modelTool ? !request.arguments || typeof request.arguments !== 'object' || Array.isArray(request.arguments)
|
|
73
|
+
: !Array.isArray(request.messages) || !request.messages.length) throw this.failure('USAGE_INPUT_INVALID')
|
|
74
|
+
const requestHash = this.hash({ serviceId, kind: modelTool ? 'tool' : 'chat', ...request })
|
|
75
|
+
const existing = this.markers(session).filter(item => item.data?.turnId === copied.userMessageId && item.data?.requestKey === requestKey)
|
|
76
|
+
if (existing.length > 1) throw this.failure('USAGE_RECOVERY_GAP')
|
|
77
|
+
if (existing.length) {
|
|
78
|
+
const meta = existing[0].data
|
|
79
|
+
if (meta.requestHash !== requestHash || meta.serviceId !== serviceId) throw this.failure('USAGE_IDEMPOTENCY_CONFLICT')
|
|
80
|
+
return { receipt: await this.inspect(session, meta, options) }
|
|
81
|
+
}
|
|
82
|
+
if (this.host.cancelled.has(`${session.id}:${copied.userMessageId}`)) throw this.failure('USAGE_CANCELLED')
|
|
83
|
+
this.signalActive(options.signal)
|
|
84
|
+
if (this.host.ended.has(`${session.id}:${copied.userMessageId}`) || !this.isLiveMessage(session, event) || session.events.some(e => e.type === CLOSED_EVENT && e.data?.id === copied.userMessageId)) throw this.failure('USAGE_TURN_ENDED')
|
|
85
|
+
// Preserve the original user/account/service credential binding alongside the selected model.
|
|
86
|
+
// The server remains authoritative for its current plan and credential scope.
|
|
87
|
+
if (modelTool) {
|
|
88
|
+
const directory = await this.host.client.modelTools(options)
|
|
89
|
+
const tool = directory.items.find(item => item.service_id === serviceId)
|
|
90
|
+
if (!tool) throw this.failure('SERVICE_NOT_ENTITLED')
|
|
91
|
+
if (!tool.callable) throw this.failure('USAGE_MODEL_TOOL_UNAVAILABLE')
|
|
92
|
+
}
|
|
93
|
+
const meta = { schema: META_SCHEMA, kind: modelTool ? 'tool' : 'chat', operationId: randomUUID(), requestKey, requestHash,
|
|
94
|
+
binding: clone(this.host.assertSupported()), serviceId, conversationId: this.exact(session.id),
|
|
95
|
+
turnId: copied.userMessageId, eventSeq: event.seq, eventHash: this.hash(event.data) }
|
|
96
|
+
const record = { schema: SCHEMA, sessionId: this.key(session, meta.operationId), revision: 0,
|
|
97
|
+
entries: [{ request: { metadata: meta, state: 'prepared' } }] }
|
|
98
|
+
await this.save(session, record)
|
|
99
|
+
if (typeof session.append !== 'function') throw this.failure('USAGE_RECOVERY_GAP')
|
|
100
|
+
session.append(MODEL_REQUEST_EVENT, meta)
|
|
101
|
+
await this.host.durable(session)
|
|
102
|
+
if (options.signal?.aborted || this.closed(session, meta)) {
|
|
103
|
+
record.entries[0].request.state = 'rejected'
|
|
104
|
+
await this.save(session, record)
|
|
105
|
+
throw this.afterFailure(this.failure(options.signal?.aborted ? 'USAGE_CANCELLED' : 'USAGE_TURN_ENDED'), meta, 'not_dispatched')
|
|
106
|
+
}
|
|
107
|
+
return { meta, request }
|
|
108
|
+
})
|
|
109
|
+
if (prepared.receipt) return prepared.receipt
|
|
110
|
+
const { meta, request } = prepared
|
|
111
|
+
const wire = { ...request, operation_id: meta.operationId, service_id: meta.serviceId,
|
|
112
|
+
conversation_id: meta.conversationId, turn_id: meta.turnId }
|
|
113
|
+
let result, receivedFrame = false
|
|
114
|
+
try {
|
|
115
|
+
await this.host.primary(session); this.signalActive(options.signal)
|
|
116
|
+
if (this.closed(session, meta)) throw this.failure('USAGE_TURN_ENDED')
|
|
117
|
+
if (modelTool) result = await this.host.client.runModelTool(wire, options)
|
|
118
|
+
else if (options.onDelta) {
|
|
119
|
+
for await (const frame of this.host.client.modelStream(wire, options)) {
|
|
120
|
+
receivedFrame = true
|
|
121
|
+
if (frame.type === 'operation') result = frame.operation
|
|
122
|
+
else if (['delta', 'provider'].includes(frame.type) && !options.signal?.aborted && !this.closed(session, meta)) {
|
|
123
|
+
await this.host.primary(session)
|
|
124
|
+
await options.onDelta({ ...frame, preview: true, executable: false })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!result) throw this.failure('USAGE_STREAM_INCOMPLETE', { feedback: { schema: 'bailing.usage-feedback.v1', code: 'USAGE_STREAM_INCOMPLETE', dispatch: 'unknown', next_action: 'inspect_original', retryable: true } })
|
|
128
|
+
} else result = await this.host.client.modelComplete(wire, options)
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (receivedFrame && error?.feedback) error.feedback = { ...error.feedback, dispatch: 'unknown', next_action: 'inspect_original' }
|
|
131
|
+
if (['USAGE_INPUT_LIMIT', 'USAGE_UNSUPPORTED', 'SERVICE_NOT_ENTITLED'].includes(error?.code)) this.host.modelModelsCache = null
|
|
132
|
+
try {
|
|
133
|
+
await this.update(session, meta, item => { item.state = error?.feedback?.dispatch === 'not_dispatched' ? 'rejected' : 'unknown' })
|
|
134
|
+
await this.host.primary(session)
|
|
135
|
+
} catch (primary) { throw this.afterFailure(primary, meta) }
|
|
136
|
+
throw this.afterFailure(error?.feedback ? error : this.failure('USAGE_TRANSPORT_UNAVAILABLE'), meta, error?.feedback?.dispatch)
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
this.check(result, meta)
|
|
140
|
+
await this.update(session, meta, item => { item.state = this.state(result) })
|
|
141
|
+
await this.host.primary(session)
|
|
142
|
+
} catch (error) { throw this.afterFailure(error, meta) }
|
|
143
|
+
return this.delivery(session, meta, result, Boolean(options.signal?.aborted))
|
|
144
|
+
}
|
|
145
|
+
afterFailure(error, meta, dispatch = 'unknown') {
|
|
146
|
+
return Object.assign(error, { operationId: meta.operationId, turnId: meta.turnId,
|
|
147
|
+
feedback: { ...error.feedback, dispatch, ...(dispatch === 'unknown' ? { next_action: 'inspect_original' } : {}) } })
|
|
148
|
+
}
|
|
149
|
+
check(result, meta) {
|
|
150
|
+
if (result?.schema !== 'bailing.model-operation.v1' || result.operation_id !== meta.operationId
|
|
151
|
+
|| result.account_id !== meta.binding.accountId || result.user_id !== meta.binding.userId
|
|
152
|
+
|| result.service_id !== meta.serviceId || result.conversation_id !== meta.conversationId || result.turn_id !== meta.turnId
|
|
153
|
+
|| !['pending', 'complete', 'unknown', 'cancelled', 'failed'].includes(result.result_state)
|
|
154
|
+
|| !['pending', 'settled'].includes(result.billing_state)) throw this.failure('USAGE_RECOVERY_GAP')
|
|
155
|
+
}
|
|
156
|
+
state(result) { return result.response_expired === true ? 'closed_unresolved' : result.result_state === 'complete' ? 'completed' : result.result_state === 'cancelled' ? 'cancelled' : result.result_state === 'failed' ? 'rejected' : 'unknown' }
|
|
157
|
+
delivery(session, meta, result, aborted = false) {
|
|
158
|
+
const closed = aborted || this.closed(session, meta), ready = result.result_state === 'complete' && result.response && typeof result.response === 'object'
|
|
159
|
+
return { ...result, host_turn_active: Boolean(!closed && ready),
|
|
160
|
+
delivery_state: closed ? 'cancelled_late_result' : ready ? 'response_ready' : result.result_state === 'failed' ? 'request_failed' : result.response_expired === true || result.result_state === 'unknown' ? 'unresolved_original' : 'pending_original',
|
|
161
|
+
...(closed ? { next_action: 'inspect_original' } : result.response_expired === true ? { next_action: 'contact_operator' } : {}) }
|
|
162
|
+
}
|
|
163
|
+
async update(session, meta, update) {
|
|
164
|
+
return this.host.serial(session, async () => {
|
|
165
|
+
await this.host.primary(session)
|
|
166
|
+
const record = await this.load(session, meta)
|
|
167
|
+
update(record.entries[0].request)
|
|
168
|
+
await this.save(session, record)
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
async inspect(session, meta, options = {}) {
|
|
172
|
+
const record = await this.load(session, meta)
|
|
173
|
+
const result = await this.host.client.inspectModelRequest(meta.operationId, { ...options, serviceId: meta.serviceId, conversationId: meta.conversationId, turnId: meta.turnId })
|
|
174
|
+
this.check(result, meta)
|
|
175
|
+
record.entries[0].request.state = this.state(result)
|
|
176
|
+
await this.save(session, record)
|
|
177
|
+
await this.host.primary(session)
|
|
178
|
+
return { ...this.delivery(session, meta, result), recovered_original: true }
|
|
179
|
+
}
|
|
180
|
+
async recover(session, operationId, options = {}) {
|
|
181
|
+
this.assertSupported()
|
|
182
|
+
return this.host.serial(session, async () => {
|
|
183
|
+
await this.host.durable(session)
|
|
184
|
+
return this.inspect(session, this.locate(session, operationId), options)
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
async close(session, userMessageId, cancelled, options = {}) {
|
|
188
|
+
const id = this.exact(userMessageId)
|
|
189
|
+
;(cancelled ? this.host.cancelled : this.host.ended).add(`${session.id}:${id}`)
|
|
190
|
+
return this.host.serial(session, async () => {
|
|
191
|
+
await this.host.primary(session)
|
|
192
|
+
this.eventFor(session, id)
|
|
193
|
+
if (!session.events.some(e => e.type === CLOSED_EVENT && e.data?.id === id)) {
|
|
194
|
+
session.append(CLOSED_EVENT, { id, cancelled: Boolean(cancelled) })
|
|
195
|
+
await this.host.durable(session)
|
|
196
|
+
}
|
|
197
|
+
const results = []
|
|
198
|
+
for (const marker of this.markers(session).filter(e => e.data?.turnId === id)) {
|
|
199
|
+
const meta = marker.data, record = await this.load(session, meta)
|
|
200
|
+
if (cancelled && !['completed', 'rejected', 'cancelled'].includes(record.entries[0].request.state)) {
|
|
201
|
+
// Original request cancellation only. No turn exists and no model operation is replayed.
|
|
202
|
+
const result = await this.host.client.cancelModelRequest(meta.operationId,
|
|
203
|
+
{ ...options, serviceId: meta.serviceId, conversationId: meta.conversationId, turnId: meta.turnId })
|
|
204
|
+
this.check(result, meta); results.push(result)
|
|
205
|
+
record.entries[0].request.state = this.state(result)
|
|
206
|
+
await this.save(session, record)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { state: cancelled ? 'cancelled' : 'ended', requests: results, model_operation_performed: false }
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
async status(session, options = {}) {
|
|
213
|
+
const pending = []
|
|
214
|
+
for (const marker of this.markers(session)) {
|
|
215
|
+
const record = await this.load(session, marker.data)
|
|
216
|
+
if (['prepared', 'unknown'].includes(record.entries[0].request.state)) pending.push(marker.data.operationId)
|
|
217
|
+
}
|
|
218
|
+
let summary
|
|
219
|
+
try { summary = await this.host.client.modelSummary(options) }
|
|
220
|
+
catch (error) { await this.host.primary(session); throw error }
|
|
221
|
+
await this.host.primary(session)
|
|
222
|
+
const presentation = summary?.presentation
|
|
223
|
+
// Hosts may provide a custom client. Never hand an old/raw summary to a customer UI.
|
|
224
|
+
if (presentation?.schema !== 'bailing.usage-presentation.v1'
|
|
225
|
+
|| !['credits', 'percentage', 'none'].includes(presentation.kind)
|
|
226
|
+
|| !['active', 'depleted', 'not_started', 'expired', 'suspended', 'unavailable'].includes(presentation.state)
|
|
227
|
+
|| !['remaining', 'total'].every(key => presentation[key] === null || typeof presentation[key] === 'number' && Number.isFinite(presentation[key]) && presentation[key] >= 0)
|
|
228
|
+
|| !(presentation.displayValue === null || typeof presentation.displayValue === 'string' && /^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,2})?$|^<(?:1|0\.01)$/.test(presentation.displayValue))) throw this.failure('USAGE_RESPONSE_INVALID')
|
|
229
|
+
const usable = ['active', 'depleted'].includes(presentation.state)
|
|
230
|
+
if (presentation.kind === 'none') {
|
|
231
|
+
if (presentation.state !== 'unavailable' || presentation.remaining !== null || presentation.total !== null || presentation.displayValue !== null) throw this.failure('USAGE_RESPONSE_INVALID')
|
|
232
|
+
} else if (!usable) {
|
|
233
|
+
if (!['not_started', 'expired', 'suspended'].includes(presentation.state) || presentation.remaining !== null || presentation.displayValue !== null) throw this.failure('USAGE_RESPONSE_INVALID')
|
|
234
|
+
} else {
|
|
235
|
+
if (presentation.remaining === null || presentation.total === null || presentation.displayValue === null
|
|
236
|
+
|| presentation.state === 'active' && presentation.remaining <= 0
|
|
237
|
+
|| presentation.state === 'depleted' && (presentation.remaining !== 0 || presentation.displayValue !== '0')) throw this.failure('USAGE_RESPONSE_INVALID')
|
|
238
|
+
if (presentation.kind === 'percentage') {
|
|
239
|
+
if (presentation.total !== 100 || presentation.remaining > 100 || !/^(?:0|[1-9][0-9]?|100|<1)$/.test(presentation.displayValue)) throw this.failure('USAGE_RESPONSE_INVALID')
|
|
240
|
+
} else if (presentation.displayValue === '<1') throw this.failure('USAGE_RESPONSE_INVALID')
|
|
241
|
+
}
|
|
242
|
+
return { schema: 'bailing.usage-host-status.v1', state: 'ready', binding: clone(this.host.assertSupported()),
|
|
243
|
+
mode: 'model_gateway', summary: clone(summary), pendingOperations: pending,
|
|
244
|
+
model_transport_required: true, byok_metered: false, turn_required: false }
|
|
245
|
+
}
|
|
246
|
+
}
|
package/lib/runtime.js
CHANGED
|
@@ -1014,6 +1014,30 @@ export class BailingHubAgentClientRuntime {
|
|
|
1014
1014
|
return this.sessionTasks.set(id, request)
|
|
1015
1015
|
}
|
|
1016
1016
|
|
|
1017
|
+
/** Synchronous snapshot; queued local writes can still be pending. */
|
|
1018
|
+
getSessionLocalPrimaryStatus(session) {
|
|
1019
|
+
if (!session || typeof session.id !== 'string' || !session.id.trim() || session.id.length > 512
|
|
1020
|
+
|| /[\u0000-\u001f\u007f]/.test(session.id) || !Array.isArray(session.events)) {
|
|
1021
|
+
throw new TypeError('A real Session with its current events is required')
|
|
1022
|
+
}
|
|
1023
|
+
const view = this.sessionTaskView(session.id, session)
|
|
1024
|
+
return { schema: 'bailing.agent-session-local-primary.v1',
|
|
1025
|
+
state: ['storage_error', 'recovery_gap'].includes(view.state) ? view.state : 'ready',
|
|
1026
|
+
local: { ...view.local }, snapshot_is_dispatch_permission: false }
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/** Settle already queued local persistence before checking the primary error. */
|
|
1030
|
+
async waitForSessionLocalPrimaryStatus(session) {
|
|
1031
|
+
this.getSessionLocalPrimaryStatus(session) // Validate before touching queues.
|
|
1032
|
+
await Promise.all([
|
|
1033
|
+
this.conversationOutbox.drain(session.id),
|
|
1034
|
+
this.invocationJournal.drain(session.id),
|
|
1035
|
+
])
|
|
1036
|
+
// Task queues also contain remote validation; never wait for them here.
|
|
1037
|
+
// Failed saves retain their original pending records and error status.
|
|
1038
|
+
return this.getSessionLocalPrimaryStatus(session)
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1017
1041
|
async getSessionTaskState(session) {
|
|
1018
1042
|
const id = typeof session === 'string' ? session : String(session.id)
|
|
1019
1043
|
await this.sessionTasks.refresh(id)
|