Merge remote-tracking branch 'origin/master' into feat/add-session-data-preview
# Conflicts: # docs/cordis-catalog/services.md # docs/core-data-structures/session.i18n.yaml # packages/client/ui-conversation/README.i18n.yaml # packages/llm/token-meter/README.i18n.yaml
This commit is contained in:
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/README.md
|
||||
README.md: 66b7beabd73cc3fec7230f209a9da0da48a37c95
|
||||
README.zh.md: 3f38c94fb4a43bed007d06b8c8558b5bd330a41c
|
||||
README.zh.md: 08f561840d1a560cfc9bced6f8757a0b0fc5770a
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
LLM seam 及其提供方适配器。接口包(`llm`)拥有抽象服务、内容块词汇和流分片组装器;适配器是在 `ctx.llm` 上注册的具体实现。这些全是**产品**包(package)。
|
||||
LLM seam 及其提供方适配器。接口包(`llm`)拥有抽象服务、内容块词汇和流分片组装器;适配器是在 `ctx.llm` 上注册的具体实现。这些全是**产品**包。
|
||||
|
||||
| 包 | 职责 | ctx key |
|
||||
|---|---|---|
|
||||
@@ -12,4 +12,4 @@ LLM seam 及其提供方适配器。接口包(`llm`)拥有抽象服务、内
|
||||
| `llm-deepseek/` | DeepSeek API 适配器,直接使用 fetch + eventsource-parser 和 SSE(Server-Sent Events) | (注册到 `ctx.llm`) |
|
||||
| `llm-pi-ai/` | 通过 `@earendil-works/pi-ai` 实现的多提供方适配器 | (注册到 `ctx.llm`) |
|
||||
|
||||
接口位于 `llm/llm/`;适配器、重试策略和可复用的 token 计量器以扁平结构并列在该分组下。请求按 `provider` 路由,而 `model` 会原样传给选中的适配器。负责该路由的适配器提供重试策略,并解析可用的确切模型身份、上下文容量和推理元数据;重试执行器与 token 计量器仍与提供方无关。新的提供方适配器只需在 `ctx.llm` 上注册一个或多个提供方路由,无需改动消费方。两个已交付实现见[双生 LLM 适配器](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md),测量归属见[回放 token 计量器 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md),容量与压缩(compaction)策略归属见[路由模型上下文 Agent Note](../../.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md)。
|
||||
接口位于 `llm/llm/`;适配器、重试策略和可复用的 token 计量器以扁平结构并列在该分组下。请求按 `provider` 路由,而 `model` 会原样传给选中的适配器。负责该路由的适配器提供重试策略,并解析可用的确切模型身份、上下文容量和推理元数据;重试执行器与 token 计量器仍与提供方无关。新的提供方适配器只需在 `ctx.llm` 上注册一个或多个提供方路由,无需改动消费方。两个已交付实现见[双生 LLM 适配器](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md),测量归属见[回放 token 计量器 Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md),容量与压缩(compaction)策略归属见[路由模型上下文 Agent Note](../../.agents/notes/implemented/architecture/2026-07-20-routed-model-context-and-compaction-policy.md)。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md
|
||||
README.md: 020aa65073495526be3f32912b7cd06667c52a2e
|
||||
README.zh.md: 4c655e90ba00340c056f6ac16159621f7a8c1ddb
|
||||
README.md: 72b04f5982ec7fdef024835ab23e7a9f0005f84c
|
||||
README.zh.md: 2c9155e0da1373e5d9f8a913c10b11ddee417b1f
|
||||
|
||||
@@ -40,7 +40,7 @@ The plugin registers the single provider route `deepseek-official` together with
|
||||
|
||||
`contextWindow` is optional per configured model and is not exposed through the advisory catalog. `ctx.llm.resolveModelInfo('deepseek-official', model).context` returns an exact model value first, then `defaultContextWindow` for an entry without capacity or an unlisted pass-through id. The adapter default is 1,000,000; pressure-sensitive plugins therefore get deployment-owned capacity without treating the model selector as authoritative. Registering another adapter for `deepseek-official` throws `LlmError('DUPLICATE_ADAPTER')`.
|
||||
|
||||
`maxTokens` is the adapter-configured output cap for conversation requests and defaults to 256,000. Exact-model resolution exposes it as `defaultMaxTokens`; `LlmService` materializes that value into `GenerateOptions.maxTokens` before the agent loop writes `request/header`, so the wire request remains reconstructable. An explicit request or `AgentOptions.maxTokens` value wins and is serialized as `max_tokens`. The adapter does not clamp this request budget against `contextWindow`; deployments with a smaller context or provider output limit must configure a compatible `maxTokens`.
|
||||
`maxTokens` is the adapter-configured output cap for conversation requests and defaults to 256,000. A catalog entry may carry its own `maxTokens`, which wins for that model; an entry without one, and any unlisted pass-through id, resolve to the profile value, so adding a per-model cap changes one model rather than the route. Exact-model resolution exposes the winner as `defaultMaxTokens`; `LlmService` materializes that value into `GenerateOptions.maxTokens` before the agent loop writes `request/header`, so the wire request remains reconstructable. An explicit request or `AgentOptions.maxTokens` value wins and is serialized as `max_tokens`. The adapter does not clamp this request budget against `contextWindow`; deployments with a smaller context or provider output limit must configure a compatible `maxTokens`.
|
||||
|
||||
The same exact-model result exposes ordered `off`, `high`, and `max` efforts under `reasoning` for every pass-through model when deployment policy permits thinking. `reasoningEffort` selects the deployment default and falls back to `high` when omitted. `agent/request` can replace it on each conversation step; the resolved value is logged in `request/header`. `high` and `max` enable thinking and serialize as the official top-level `reasoning_effort`; adapter-owned `off` instead serializes `thinking.type: disabled` and omits `reasoning_effort`. An unsupported value fails with `UNSUPPORTED_REASONING_EFFORT` before network I/O.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:直接 `fetch` + SSE(Server-Sent Events,由 `eventsource-parser` 分帧),将官方协议格式(wire format;真源:API 文档 guides/thinking_mode、guides/tool_calls、api/create-chat-completion)转换为 `StreamChunk` 协议。
|
||||
|
||||
同一 seam 的第二个基于库的实现位于 `@deepseek-ai/dsh-llm-pi-ai`。本包(package)拥有 `deepseek-official` 提供方路由——刻意区别于 pi-ai 的 catalog 名称 `deepseek`,因此同一组合可以并排挂载两条 DeepSeek 路径;而为 `deepseek-official` 本身注册另一个适配器仍会抛出 `LlmError('DUPLICATE_ADAPTER')`。
|
||||
同一 seam 的第二个基于库的实现位于 `@deepseek-ai/dsh-llm-pi-ai`。本包拥有 `deepseek-official` 提供方路由——刻意区别于 pi-ai 的 catalog 名称 `deepseek`,因此同一组合可以并排挂载两条 DeepSeek 路径;而为 `deepseek-official` 本身注册另一个适配器仍会抛出 `LlmError('DUPLICATE_ADAPTER')`。
|
||||
|
||||
包根入口导出 Cordis 插件契约与 `DeepSeekAdapter`;协议序列化、SSE 解析与分片转换 helper 不属于该根契约。
|
||||
|
||||
@@ -40,7 +40,7 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:
|
||||
|
||||
`contextWindow` 对每个已配置模型都可选,不会通过建议 catalog 公开。`ctx.llm.resolveModelInfo('deepseek-official', model).context` 先返回精确模型值,再对不含容量的配置项或未列出原样传递 id 返回 `defaultContextWindow`。适配器默认值为 1,000,000;因此,压力敏感插件可以获得由部署决定的容量,不会将模型 selector 视为权威。为 `deepseek-official` 注册另一个适配器会抛出 `LlmError('DUPLICATE_ADAPTER')`。
|
||||
|
||||
`maxTokens` 是适配器为对话请求配置的输出上限,默认值为 256,000。确切模型解析会将其公开为 `defaultMaxTokens`;`LlmService` 会在 agent loop(智能体循环)写入 `request/header` 前,将该值填入 `GenerateOptions.maxTokens`,从而仍可根据持久记录重建协议请求。显式的请求值或 `AgentOptions.maxTokens` 值优先,并会序列化为 `max_tokens`。适配器不会根据 `contextWindow` 自动调低该请求预算;上下文或提供方输出上限较小的部署必须配置与其相容的 `maxTokens`。
|
||||
`maxTokens` 是适配器为对话请求配置的输出上限,默认值为 256,000。Catalog 配置项可以自带 `maxTokens`,它对该模型胜出;不含该上限的配置项以及任何未列出原样传递 id 都解析为 profile 值,因此新增按模型的上限只改变一个模型,而非整条路由。确切模型解析会将胜出值公开为 `defaultMaxTokens`;`LlmService` 会在 agent loop(智能体循环)写入 `request/header` 前,将该值填入 `GenerateOptions.maxTokens`,从而仍可根据持久记录重建协议请求。显式的请求值或 `AgentOptions.maxTokens` 值优先,并会序列化为 `max_tokens`。适配器不会根据 `contextWindow` 自动调低该请求预算;上下文或提供方输出上限较小的部署必须配置与其相容的 `maxTokens`。
|
||||
|
||||
同一确切模型结果会在部署策略允许思考时,为每个原样传递模型在 `reasoning` 下公开有序的 `off`、`high` 和 `max` 推理(reasoning)强度。`reasoningEffort` 选择部署默认值,省略时回退为 `high`。`agent/request` 可以在每个会话步骤替换它;解析后的值会记录在 `request/header`。`high` 和 `max` 会启用思考,并序列化为官方顶层 `reasoning_effort`;适配器持有的 `off` 则序列化为 `thinking.type: disabled`,且省略 `reasoning_effort`。不支持的值会在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败。
|
||||
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface DeepSeekCatalogModel {
|
||||
description?: string
|
||||
/** Known combined request/response context capacity; omitted when deployment metadata is unavailable. */
|
||||
contextWindow?: number
|
||||
/** Per-request output cap for this model; omission falls back to the profile's {@link DeepSeekConnectionOptions.maxTokens}. */
|
||||
maxTokens?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,7 +183,7 @@ export class DeepSeekAdapter extends LlmAdapter {
|
||||
? { provider, id: model, name: model }
|
||||
: modelInfo(provider, configured),
|
||||
context: { contextWindow },
|
||||
defaultMaxTokens: connection.maxTokens,
|
||||
defaultMaxTokens: configured?.maxTokens ?? connection.maxTokens,
|
||||
...connection.defaults.thinking === 'disabled'
|
||||
? {
|
||||
reasoning: {
|
||||
|
||||
@@ -68,7 +68,7 @@ export interface Config {
|
||||
thinking?: 'enabled' | 'disabled'
|
||||
/** Default thinking effort (default `high`); `off` disables thinking per request. */
|
||||
reasoningEffort?: 'off' | 'high' | 'max'
|
||||
/** Default per-request output cap (default 256,000); explicit request values win. */
|
||||
/** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */
|
||||
maxTokens?: number
|
||||
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
|
||||
defaultContextWindow?: number
|
||||
@@ -85,6 +85,7 @@ const catalogModel: z<DeepSeekCatalogModel> = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
contextWindow: z.number().step(1).min(1),
|
||||
maxTokens: z.number().step(1).min(1),
|
||||
})
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
@@ -125,6 +126,12 @@ function resolveModels(models: readonly DeepSeekCatalogModel[] | undefined): Dee
|
||||
`llm-deepseek: catalog model "${model.id}" contextWindow must be a positive integer`,
|
||||
)
|
||||
}
|
||||
if (model.maxTokens !== undefined
|
||||
&& (!Number.isInteger(model.maxTokens) || model.maxTokens <= 0)) {
|
||||
throw new Error(
|
||||
`llm-deepseek: catalog model "${model.id}" maxTokens must be a positive integer`,
|
||||
)
|
||||
}
|
||||
if (seen.has(model.id)) throw new Error(`llm-deepseek: duplicate catalog model "${model.id}"`)
|
||||
seen.add(model.id)
|
||||
return {
|
||||
@@ -132,6 +139,7 @@ function resolveModels(models: readonly DeepSeekCatalogModel[] | undefined): Dee
|
||||
...model.name === undefined ? {} : { name: model.name },
|
||||
...model.description === undefined ? {} : { description: model.description },
|
||||
...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
|
||||
...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -793,6 +793,26 @@ describe('plugin registration and config', () => {
|
||||
expect(ctx.llm.listProviders()).toEqual([])
|
||||
})
|
||||
|
||||
it.each([0, 1.5])('rejects a per-model output cap of %s', (maxTokens) => {
|
||||
expect(() => resolveAdapterOptions({ models: [{ id: 'bad-cap', maxTokens }] }))
|
||||
.toThrow(/maxTokens must be a positive integer/)
|
||||
})
|
||||
|
||||
it('prefers a model\'s own output cap over the profile default', async () => {
|
||||
// The profile default stays what an unlisted or uncapped model resolves
|
||||
// to, so adding a per-model cap changes one model rather than the route.
|
||||
const adapter = adapterOf({ maxTokens: 4096, models: [
|
||||
{ id: 'capped', maxTokens: 512 },
|
||||
{ id: 'uncapped' },
|
||||
] })
|
||||
await expect(adapter.resolveModel('deepseek-official', 'capped'))
|
||||
.resolves.toMatchObject({ defaultMaxTokens: 512 })
|
||||
await expect(adapter.resolveModel('deepseek-official', 'uncapped'))
|
||||
.resolves.toMatchObject({ defaultMaxTokens: 4096 })
|
||||
await expect(adapter.resolveModel('deepseek-official', 'not-in-catalog'))
|
||||
.resolves.toMatchObject({ defaultMaxTokens: 4096 })
|
||||
})
|
||||
|
||||
it('rejects invalid context capacity when apply is called directly', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md
|
||||
README.md: e8c2682cbb72ca1ac6a5ad6b26bdf63f0695716b
|
||||
README.zh.md: 5fb19ee1343e905352609d96e7f540c1a411b4d8
|
||||
README.zh.md: 4175b3a751affa65ac68284c7ead47b1f71b5e15
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
基于 [`@earendil-works/pi-ai`](https://www.npmjs.com/package/@earendil-works/pi-ai) 的 harness LLM(大语言模型)seam 通用多提供方适配器。一个插件实例拥有一份以路由为键的提供方 profile 字典;每个请求使用 `GenerateOptions.provider` 选择 profile,并从 pi-ai 已安装 catalog 中动态解析 `GenerateOptions.model`。
|
||||
|
||||
包(package)根入口导出 Cordis 插件契约与 `PiAiAdapter`;profile 解析、模型构造、回放转换和流转换保留在包内部。
|
||||
包根入口导出 Cordis 插件契约与 `PiAiAdapter`;profile 解析、模型构造、回放转换和流转换保留在包内部。
|
||||
|
||||
## 配置
|
||||
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -25,9 +25,7 @@
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/llm/README.md
|
||||
README.md: 21f428fb22c9a59a67d86f446ea866c1629b964a
|
||||
README.zh.md: f6421f0625de7e63432a4863db6fb2d96ecd1b3f
|
||||
README.zh.md: 9bd26993bc63b39de3d3a8039fea6b046c875504
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
提供方无关的 LLM(大语言模型)词汇与抽象服务。本包(package)定义 agent loop(智能体循环)、会话日志和每个插件使用的规范语言。
|
||||
提供方无关的 LLM(大语言模型)词汇与抽象服务。本包定义 agent loop(智能体循环)、会话日志和每个插件使用的规范语言。
|
||||
|
||||
## 服务:`LlmService`(ctx key:`llm`)
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
### 应用归因(`attribution.ts`)
|
||||
|
||||
每个产品适配器都会在提供方 HTTP 请求上发送应用身份。`attributionHeaders(identity?)` 构建标准 `User-Agent`,默认为公开 `APP_IDENTITY`;白标部署可以替换它,但不能抑制它。适配器会直接验证 wire 标头,或通过自身库 hook 验证。详见 [归因 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md)。
|
||||
每个产品适配器都会在提供方 HTTP 请求上发送应用身份。`attributionHeaders(identity?)` 构建标准 `User-Agent`,默认为公开 `APP_IDENTITY`;白标部署可以替换它,但不能抑制它。适配器会直接验证 wire 标头,或通过自身库 hook 验证。详见 [归因 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md)。
|
||||
|
||||
### 类
|
||||
|
||||
|
||||
@@ -34,9 +34,7 @@
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/token-meter/README.md
|
||||
README.md: 2b8320221dc314c9a18f93eab3d88a66d22d8342
|
||||
README.zh.md: 7c0a433146013e79034d5eb34ebb39e93caf30ff
|
||||
README.zh.md: ce2e352b8a4191bfbfcce4f317d21e3db512abfd
|
||||
|
||||
@@ -39,7 +39,7 @@ fold 跟踪完整请求标头快照、步骤边界、表层追加与替换、成
|
||||
|
||||
这是刻意的选择。占用率百分比是面向用户的参考数字,既不是计费记录,也不是门控输入:harness 中没有任何环节依据它做决策,压缩改为直接读取 `measure()`。TUI 状态行一直以同样的方式计算占用率,即用 `measure()` 总量除以为所选模型单独解析出的容量。
|
||||
|
||||
让这对值保持原子已经尝试过并被否决:它需要一个临时且不可回放的协议帧,进而需要针对跨流重排序的生命周期栅栏,还会让占用率在每次重连后变为空白。[Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md)记录了这项对比。需要同一边界精确数字的消费方应在自己的请求边界调用 `measure()`,而不是读取该投影。
|
||||
让这对值保持原子已经尝试过并被否决:它需要一个临时且不可回放的协议帧,进而需要针对跨流重排序的生命周期栅栏,还会让占用率在每次重连后变为空白。[Agent Note](../../../.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md)记录了这项对比。需要同一边界精确数字的消费方应在自己的请求边界调用 `measure()`,而不是读取该投影。
|
||||
|
||||
## 组合
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -147,7 +147,7 @@ describe('TokenMeterService pricing', () => {
|
||||
|
||||
it('returns a detached deeply immutable empty measurement', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('empty'))
|
||||
const session = Session.create(SessionId('empty'))
|
||||
const result = service.measure(session)
|
||||
expect(result).toEqual({
|
||||
logRevision: 0,
|
||||
@@ -168,7 +168,7 @@ describe('TokenMeterService pricing', () => {
|
||||
|
||||
it('keeps an earlier unified snapshot detached from later replay', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('detached'))
|
||||
const session = Session.create(SessionId('detached'))
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'first' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -200,7 +200,7 @@ describe('TokenMeterService pricing', () => {
|
||||
|
||||
it('prices header, tools, and surface when no reusable usage exists', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('heuristic'))
|
||||
const session = Session.create(SessionId('heuristic'))
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'question' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -218,7 +218,7 @@ describe('TokenMeterService pricing', () => {
|
||||
|
||||
it('keeps request-header overrides out of the returned surface', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('override-surface'))
|
||||
const session = Session.create(SessionId('override-surface'))
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'question' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -246,7 +246,7 @@ describe('replay anchors and surface folds', () => {
|
||||
|
||||
it('uses disjoint provider usage and signed durable-output rewrites', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('usage'))
|
||||
const session = Session.create(SessionId('usage'))
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'before' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -267,7 +267,7 @@ describe('replay anchors and surface folds', () => {
|
||||
|
||||
it('selects a heuristic anchor when provider usage would undercut its scale', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('low-usage-anchor'))
|
||||
const session = Session.create(SessionId('low-usage-anchor'))
|
||||
const system = 'system context'
|
||||
const requestHeader = header('deepseek-v4-flash', { system })
|
||||
appendSuccessfulCall(session, requestHeader, {
|
||||
@@ -297,7 +297,7 @@ describe('replay anchors and surface folds', () => {
|
||||
|
||||
it('uses an estimated anchor when provider usage is absent', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('missing-usage'))
|
||||
const session = Session.create(SessionId('missing-usage'))
|
||||
appendSuccessfulCall(session, header('deepseek-v4-flash', { system: 's' }), {
|
||||
providerText: 'provider',
|
||||
durableText: 'rewritten',
|
||||
@@ -314,8 +314,8 @@ describe('replay anchors and surface folds', () => {
|
||||
})
|
||||
|
||||
it('distinguishes explicit empty provenance from absent legacy provenance', () => {
|
||||
const explicit = new Session(SessionId('explicit-empty'))
|
||||
const legacy = new Session(SessionId('legacy-absent'))
|
||||
const explicit = Session.create(SessionId('explicit-empty'))
|
||||
const legacy = Session.create(SessionId('legacy-absent'))
|
||||
appendSuccessfulCall(explicit, header('deepseek-v4-flash'), {
|
||||
durableText: 'listener injected text',
|
||||
providerText: '',
|
||||
@@ -335,7 +335,7 @@ describe('replay anchors and surface folds', () => {
|
||||
|
||||
it('keeps only the latest successful request anchor across model switches', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('switch'))
|
||||
const session = Session.create(SessionId('switch'))
|
||||
const alphaHeader = header('alpha', { system: 'same envelope' })
|
||||
appendSuccessfulCall(session, alphaHeader, { usage: USAGE, providerText: 'alpha' })
|
||||
expect(service.measure(session).baseline).toMatchObject({ kind: 'usage', tokens: 34 })
|
||||
@@ -356,7 +356,7 @@ describe('replay anchors and surface folds', () => {
|
||||
|
||||
it('invalidates usage for any canonical envelope change or explicit override', () => {
|
||||
const service = meter()
|
||||
const session = new Session(SessionId('envelope'))
|
||||
const session = Session.create(SessionId('envelope'))
|
||||
const anchoredHeader = header('deepseek-v4-flash', { system: 'one' })
|
||||
appendSuccessfulCall(session, anchoredHeader, { usage: USAGE })
|
||||
expect(service.measure(session, { ...anchoredHeader, tools: [] }).baseline.kind).toBe('usage')
|
||||
@@ -375,7 +375,7 @@ describe('replay anchors and surface folds', () => {
|
||||
})
|
||||
|
||||
it('folds the latest full header snapshot into the effective envelope', () => {
|
||||
const session = new Session(SessionId('header-snapshot'))
|
||||
const session = Session.create(SessionId('header-snapshot'))
|
||||
appendHeader(session, header('deepseek-v4-flash'))
|
||||
session.append('request/header', {
|
||||
header: header('deepseek-v4-pro'),
|
||||
@@ -388,7 +388,7 @@ describe('replay anchors and surface folds', () => {
|
||||
|
||||
it('replays seeded append and replace operations with signed deltas', () => {
|
||||
const service = meter()
|
||||
const original = new Session(SessionId('surface-original'))
|
||||
const original = Session.create(SessionId('surface-original'))
|
||||
appendSuccessfulCall(original, header('deepseek-v4-flash'), {
|
||||
usage: USAGE,
|
||||
providerText: 'long provider answer '.repeat(100),
|
||||
@@ -397,7 +397,7 @@ describe('replay anchors and surface folds', () => {
|
||||
content: [{ type: 'text', text: 'new tail' }],
|
||||
source: { kind: 'user' },
|
||||
}), { surfaceOp: 'append' })
|
||||
const seeded = new Session(SessionId('surface-seeded'), original.events)
|
||||
const seeded = Session.create(SessionId('surface-seeded'), original.events)
|
||||
const before = service.measure(seeded)
|
||||
expect(before.nodes).toHaveLength(2)
|
||||
expect(before.surfaceDeltaTokens).toBeGreaterThan(0)
|
||||
@@ -423,7 +423,7 @@ describe('replay anchors and surface folds', () => {
|
||||
})
|
||||
|
||||
it('prices an empty assistant surface anchor as zero', () => {
|
||||
const session = new Session(SessionId('empty-assistant'))
|
||||
const session = Session.create(SessionId('empty-assistant'))
|
||||
appendSuccessfulCall(session, header('deepseek-v4-flash'), {
|
||||
providerText: '',
|
||||
durableText: '',
|
||||
@@ -444,7 +444,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
}
|
||||
|
||||
it('rejects an assistant without its step boundary transactionally', () => {
|
||||
const session = new Session(SessionId('bad-step'))
|
||||
const session = Session.create(SessionId('bad-step'))
|
||||
appendHeader(session, header('deepseek-v4-flash'))
|
||||
session.append('assistant/message', {
|
||||
turn: 1,
|
||||
@@ -462,7 +462,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
})
|
||||
|
||||
it('clears completed step boundaries and rejects overlapping or late step events', () => {
|
||||
const overlapping = new Session(SessionId('overlapping-step'))
|
||||
const overlapping = Session.create(SessionId('overlapping-step'))
|
||||
overlapping.append('step/start', { turn: 1, step: 1 })
|
||||
overlapping.append('step/start', { turn: 1, step: 2 })
|
||||
expectRepeatedFailure(
|
||||
@@ -471,7 +471,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
/arrived before turn 1\/step 1 ended/,
|
||||
)
|
||||
|
||||
const late = new Session(SessionId('late-assistant'))
|
||||
const late = Session.create(SessionId('late-assistant'))
|
||||
late.append('step/start', { turn: 1, step: 1 })
|
||||
appendHeader(late, header('deepseek-v4-flash'))
|
||||
late.append('step/end', { turn: 1, step: 1 })
|
||||
@@ -493,7 +493,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
/no matching step\/start/,
|
||||
)
|
||||
|
||||
const mismatchedEnd = new Session(SessionId('mismatched-end'))
|
||||
const mismatchedEnd = Session.create(SessionId('mismatched-end'))
|
||||
mismatchedEnd.append('step/start', { turn: 1, step: 1 })
|
||||
mismatchedEnd.append('step/end', { turn: 1, step: 2 })
|
||||
expectRepeatedFailure(
|
||||
@@ -532,7 +532,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
},
|
||||
]
|
||||
for (const testCase of cases) {
|
||||
const session = new Session(SessionId(`bad-source-${testCase.name}`))
|
||||
const session = Session.create(SessionId(`bad-source-${testCase.name}`))
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
appendHeader(session, header('deepseek-v4-flash'))
|
||||
const sourceEventSeqs = testCase.appendSource(session)
|
||||
@@ -554,7 +554,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
})
|
||||
|
||||
it('rejects repeated and non-earlier assistant provenance', () => {
|
||||
const duplicate = new Session(SessionId('duplicate-source'))
|
||||
const duplicate = Session.create(SessionId('duplicate-source'))
|
||||
duplicate.append('step/start', { turn: 1, step: 1 })
|
||||
appendHeader(duplicate, header('deepseek-v4-flash'))
|
||||
const source = duplicate.append('assistant/chunk', {
|
||||
@@ -584,7 +584,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
})
|
||||
expect(() => meter().measure(duplicate)).toThrow(/repeats source seq/)
|
||||
|
||||
const future = new Session(SessionId('future-source'))
|
||||
const future = Session.create(SessionId('future-source'))
|
||||
future.append('step/start', { turn: 1, step: 1 })
|
||||
appendHeader(future, header('deepseek-v4-flash'))
|
||||
appendUnchecked(future, {
|
||||
@@ -611,7 +611,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
})
|
||||
|
||||
it('does not partially apply a malformed assistant replacement', () => {
|
||||
const session = new Session(SessionId('transactional-replace'))
|
||||
const session = Session.create(SessionId('transactional-replace'))
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'head' }],
|
||||
source: { kind: 'user' },
|
||||
@@ -638,7 +638,7 @@ describe('malformed replay and listener lifecycle', () => {
|
||||
})
|
||||
|
||||
it('rejects corrupt replacement ranges without advancing the replay cursor', () => {
|
||||
const session = new Session(SessionId('bad-replace'))
|
||||
const session = Session.create(SessionId('bad-replace'))
|
||||
const head = session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'head' }],
|
||||
source: { kind: 'user' },
|
||||
|
||||
Reference in New Issue
Block a user