Merge commit '7baf2c33efb98eeea690c5ac58579c95dc52edd1' into feat/subagent-report-semantics
This commit is contained in:
@@ -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/support/llm-mock-server/README.md
|
||||
README.md: a535c086bf688ad48b1a3bb19c7b81da21cbad92
|
||||
README.zh.md: d814d8815b38bb34bd0d871d552e6f3ec75e042a
|
||||
README.md: 919bbc923110459e9ee4fc26f6db7e8b35ab1abb
|
||||
README.zh.md: d60c6b2395c14c0cf690c2a94e902eb1cfeffa94
|
||||
|
||||
@@ -23,7 +23,7 @@ Point the shipping DeepSeek adapter at the server; it appends `/chat/completions
|
||||
```sh
|
||||
DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1 \
|
||||
DEEPSEEK_API_KEY=mock-key \
|
||||
pnpm run demo:headless "test provider recovery"
|
||||
pnpm dsh --profile headless "test provider recovery"
|
||||
```
|
||||
|
||||
The repository script writes JSONL to stdout: a `ready` record carries the `/v1` base URL and random seed, followed by request/result records that name both the scripted behavior and the concrete selected behavior. The private support package exposes no installable binary.
|
||||
|
||||
@@ -23,7 +23,7 @@ pnpm run mock:llm -- \
|
||||
```sh
|
||||
DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1 \
|
||||
DEEPSEEK_API_KEY=mock-key \
|
||||
pnpm run demo:headless "test provider recovery"
|
||||
pnpm dsh --profile headless "test provider recovery"
|
||||
```
|
||||
|
||||
仓库脚本将 JSONL 写入 stdout:`ready` 记录携带以 `/v1` 结尾的基础 URL 和随机种子,后续请求/结果记录同时命名脚本行为和实际选中的具体行为。这个私有支持包不公开可安装的二进制命令。
|
||||
|
||||
@@ -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/support/llm-replay/README.md
|
||||
README.md: ae7cb5414a28bcf33f9878f6f02220861df9a0d5
|
||||
README.zh.md: 2acc0ff8e8c011126452ba3458aaeb0800d9d8f3
|
||||
README.md: 6119407c06cf734166300f5f70a3fe65d026d08d
|
||||
README.zh.md: 75d74b654a9a53e7ada5cc854e275eadcbe01284
|
||||
|
||||
@@ -29,7 +29,7 @@ Replay keys every call by its calling session id (`GenerateOptions.sessionId`, s
|
||||
| `file` | string | `$DSH_SNAPSHOT_FILE` | Path to the primary (parent) `session.jsonl` fixture. Required (config or env). |
|
||||
| `overrideFile` | string | `$DSH_SNAPSHOT_OVERRIDE` | Optional `ReplayOverrideDoc` sidecar for the primary session: a bare `ReplayEntry[]` replaces its derived script, while `{ patches }` augments it by call index. |
|
||||
| `childFiles` | string[] | `$DSH_SNAPSHOT_CHILD_FILES` (path-delimited) | Recorded subagent child-session logs for a nested scenario; empty for a single-session scenario. |
|
||||
| `providers` | `ReplayProviderConfig[]` | — | Optional replay-only provider and model catalog. Each provider may set `retryPolicy`, and each model may publish `contextWindow`; configured routes dispatch through the replay adapter and never perform provider I/O. |
|
||||
| `providers` | `ReplayProviderConfig[]` | — | Optional replay-only provider and model catalog. Each provider may set `retryPolicy`, and each model may publish `contextWindow` and an `inputModalities` array containing only `text` and `image`; invalid modalities fail during plugin loading. Configured routes dispatch through the replay adapter and never perform provider I/O. |
|
||||
| `paceMs` | number | — (burst) | Optional per-chunk delay in ms so downstream transports (e.g. the web SSE mux observed by a real browser) see genuinely incremental delivery. A realism knob only — tests must not depend on it for correctness. Non-negative integer; abort during a pace wait cancels the stream promptly. |
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -29,7 +29,7 @@ fixture 就是持久化的会话日志(`<scenario>/session.jsonl`)。其 `as
|
||||
| `file` | string | `$DSH_SNAPSHOT_FILE` | 主(父)`session.jsonl` fixture 的路径。必需(配置或 env)。 |
|
||||
| `overrideFile` | string | `$DSH_SNAPSHOT_OVERRIDE` | 主会话的可选 `ReplayOverrideDoc` 伴随文件:裸 `ReplayEntry[]` 替换其派生脚本,`{ patches }` 则按调用索引增补该脚本。 |
|
||||
| `childFiles` | string[] | `$DSH_SNAPSHOT_CHILD_FILES`(以路径分隔符分隔) | 嵌套场景中已记录的 subagent 子会话日志;单会话场景为空。 |
|
||||
| `providers` | `ReplayProviderConfig[]` | 无 | 可选的仅回放提供方和模型目录。每个提供方可以设置 `retryPolicy`,每个模型可以发布 `contextWindow`;已配置路由通过回放适配器分派,绝不执行提供方 I/O。 |
|
||||
| `providers` | `ReplayProviderConfig[]` | 无 | 可选的仅回放提供方和模型目录。每个提供方可以设置 `retryPolicy`,每个模型可以发布 `contextWindow` 和仅包含 `text`、`image` 的 `inputModalities` 数组;模态配置无效时,插件加载会失败。已配置路由通过回放适配器分派,绝不执行提供方 I/O。 |
|
||||
| `paceMs` | number | 无(突发) | 可选的每分片毫秒延迟,使下游传输(例如真实浏览器观察到的 Web SSE(Server-Sent Events)多路复用器)看到真正的增量传递。它只是仿真开关,测试不得依赖它保证正确性。值必须是非负整数;pace 等待期间中止会迅速取消流。 |
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
LlmModelInfo,
|
||||
LlmProviderInfo,
|
||||
LlmResolvedModelInfo,
|
||||
ModelModality,
|
||||
ResolvedRetryPolicy,
|
||||
RetryPolicyConfig,
|
||||
StreamChunk,
|
||||
@@ -51,6 +52,8 @@ export interface ReplayModelConfig {
|
||||
description?: string
|
||||
/** Optional positive integer context capacity published by the replay adapter. */
|
||||
contextWindow?: number
|
||||
/** Optional declared input modalities, so a scenario can exercise capability gates (e.g. image-capable `read_image`). */
|
||||
inputModalities?: readonly ModelModality[]
|
||||
/**
|
||||
* Optional per-request output cap the replay route materializes when callers
|
||||
* omit one, so replay reconstructs the request header a live catalog produced.
|
||||
@@ -581,6 +584,7 @@ class ReplayAdapter extends LlmAdapter {
|
||||
id: model.id,
|
||||
name: model.name ?? model.id,
|
||||
...model.description === undefined ? {} : { description: model.description },
|
||||
...model.inputModalities === undefined ? {} : { inputModalities: [...model.inputModalities] },
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -594,6 +598,9 @@ class ReplayAdapter extends LlmAdapter {
|
||||
id: model,
|
||||
name: configuredModel?.name ?? model,
|
||||
...configuredModel?.description === undefined ? {} : { description: configuredModel.description },
|
||||
...configuredModel?.inputModalities === undefined
|
||||
? {}
|
||||
: { inputModalities: [...configuredModel.inputModalities] },
|
||||
...configuredModel?.contextWindow === undefined
|
||||
? {}
|
||||
: { context: { contextWindow: configuredModel.contextWindow } },
|
||||
@@ -783,11 +790,28 @@ export interface Config {
|
||||
paceMs?: number
|
||||
}
|
||||
|
||||
function validateConfiguredModalities(providers: ReplayProviderConfig[] | undefined): void {
|
||||
for (const provider of providers ?? []) {
|
||||
for (const model of provider.models ?? []) {
|
||||
const modalities: unknown = model.inputModalities
|
||||
if (modalities === undefined) continue
|
||||
if (!Array.isArray(modalities)
|
||||
|| !modalities.every((modality: unknown) => modality === 'text' || modality === 'image')) {
|
||||
throw new Error(
|
||||
`llm-replay: provider "${provider.id}" model "${model.id}" inputModalities `
|
||||
+ 'must be an array containing only "text" and "image"',
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config = {}): void {
|
||||
const file = config.file ?? process.env.DSH_SNAPSHOT_FILE
|
||||
if (file === undefined || file.length === 0) {
|
||||
throw new Error('llm-replay: a fixture path is required (Config.file or $DSH_SNAPSHOT_FILE)')
|
||||
}
|
||||
validateConfiguredModalities(config.providers)
|
||||
const overrideFile = config.overrideFile ?? process.env.DSH_SNAPSHOT_OVERRIDE
|
||||
const childEnv = process.env.DSH_SNAPSHOT_CHILD_FILES
|
||||
const childFiles = config.childFiles
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { CompactionId } from '@deepseek-ai/dsh-compact'
|
||||
import LlmService, { CallId, createUserMessage, GenerateOptions, LlmAdapter, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import {
|
||||
type Config,
|
||||
type ReplayEntry,
|
||||
type SessionScript,
|
||||
apply,
|
||||
@@ -595,6 +596,7 @@ describe('installLlmReplay (through the real LlmService)', () => {
|
||||
{
|
||||
id: 'flash',
|
||||
contextWindow: 128_000,
|
||||
inputModalities: ['text', 'image'],
|
||||
defaultMaxTokens: 64_000,
|
||||
reasoningEfforts: ['off', 'max'],
|
||||
defaultReasoningEffort: 'max',
|
||||
@@ -611,18 +613,20 @@ describe('installLlmReplay (through the real LlmService)', () => {
|
||||
{ id: 'empty', name: 'empty' },
|
||||
])
|
||||
await expect(ctx.llm.listModels('deepseek')).resolves.toEqual([
|
||||
{ provider: 'deepseek', id: 'flash', name: 'flash' },
|
||||
{ provider: 'deepseek', id: 'flash', name: 'flash', inputModalities: ['text', 'image'] },
|
||||
{ provider: 'deepseek', id: 'pro', name: 'Pro', description: 'Larger model' },
|
||||
])
|
||||
await expect(ctx.llm.listModels('empty')).resolves.toEqual([])
|
||||
await expect(ctx.llm.resolveModelInfo('deepseek', 'flash')).resolves.toMatchObject({
|
||||
context: { contextWindow: 128_000 },
|
||||
inputModalities: ['text', 'image'],
|
||||
defaultMaxTokens: 64_000,
|
||||
reasoning: {
|
||||
efforts: [{ id: 'off', name: 'off' }, { id: 'max', name: 'max' }],
|
||||
defaultEffort: 'max',
|
||||
},
|
||||
})
|
||||
await expect(ctx.llm.resolveModelInfo('deepseek', 'pro')).resolves.not.toHaveProperty('inputModalities')
|
||||
await expect(ctx.llm.resolveModelInfo('deepseek', 'pro')).resolves.not.toHaveProperty('context')
|
||||
// Efforts without a configured default preserve the provider's own default.
|
||||
await expect(ctx.llm.resolveModelInfo('deepseek', 'pro')).resolves.toMatchObject({
|
||||
@@ -1111,11 +1115,31 @@ describe('apply (the plugin entry)', () => {
|
||||
writeFileSync(file, sessionJsonl(TEXT_CHUNKS.map((c, i) => chunkEvent(i + 1, 1, 1, c))), 'utf8')
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
apply(ctx, { file, providers: [{ id: 'm', models: [{ id: 'm' }] }], paceMs: 1 })
|
||||
expect(ctx.llm.listProviders()).toEqual([{ id: 'm', name: 'm' }])
|
||||
apply(ctx, {
|
||||
file,
|
||||
providers: [
|
||||
{ id: 'm', models: [{ id: 'm', inputModalities: ['image'] }, { id: 'text' }] },
|
||||
{ id: 'empty' },
|
||||
],
|
||||
paceMs: 1,
|
||||
})
|
||||
expect(ctx.llm.listProviders()).toEqual([{ id: 'm', name: 'm' }, { id: 'empty', name: 'empty' }])
|
||||
await expect(ctx.llm.resolveModelInfo('m', 'm')).resolves.toMatchObject({ inputModalities: ['image'] })
|
||||
expect(await drain(ctx.llm.stream({ provider: 'm', model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a string', 'image'],
|
||||
['an unknown modality', ['audio']],
|
||||
])('rejects inputModalities configured as %s during load', (_case, inputModalities) => {
|
||||
const ctx = new Context()
|
||||
const providers = [{ id: 'm', models: [{ id: 'm', inputModalities }] }] as unknown as
|
||||
NonNullable<Config['providers']>
|
||||
expect(() => { apply(ctx, { file, providers }) }).toThrow(
|
||||
'llm-replay: provider "m" model "m" inputModalities must be an array containing only "text" and "image"',
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to $DSH_SNAPSHOT_FILE / $DSH_SNAPSHOT_OVERRIDE when config is empty', async () => {
|
||||
writeFileSync(file, sessionJsonl([]), 'utf8')
|
||||
const overrideFile = join(dir, 'replay.override.json')
|
||||
|
||||
Reference in New Issue
Block a user