refactor(web): publish transient model request capacity (round 1)

This commit is contained in:
Hypatia May
2026-07-28 18:35:39 +08:00
parent 3f0ba77bfa
commit 35b9c454e5
54 changed files with 765 additions and 1352 deletions

View File

@@ -7,8 +7,10 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import LlmService, { LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmModelReasoningInfo, LlmResolvedModelInfo } from '@deepseek-ai/dsh-llm'
import LlmService, { LlmAdapter, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import type {
GenerateOptions, LlmModelReasoningInfo, LlmResolvedModelInfo, StreamChunk,
} from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId, foldRequestHeader } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
@@ -179,6 +181,7 @@ describe('request stability across the loop', () => {
provider,
id: model,
name: model,
context: { contextWindow: 64_000 },
reasoning: await reasoning.promise,
}
}
@@ -189,6 +192,12 @@ describe('request stability across the loop', () => {
})
const disposeFirst = ctx.llm.registerAdapter(['mock'], first)
const agent = ctx.agentLoop.create(SessionId('effort-hmr'), { provider: 'mock', model: 'mock' })
const dispatched: number[] = []
ctx.on('agent/model-request', (subject, _turn, _step, request) => {
if (subject === agent && request.contextWindow !== undefined) {
dispatched.push(request.contextWindow)
}
})
send(agent, 'go')
await started.promise
@@ -204,6 +213,7 @@ describe('request stability across the loop', () => {
ReasoningEffortId('high'),
])
expect(second.requests).toHaveLength(0)
expect(dispatched).toEqual([64_000])
const headers = agent.session.events.filter(event => event.type === 'request/header')
expect(headers.at(-1)?.data.header.config.reasoningEffort).toBe(ReasoningEffortId('high'))
})
@@ -308,6 +318,94 @@ describe('request stability across the loop', () => {
})
})
it('notifies one contained live model-request edge only after successful stream construction', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: 'stable base' })
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
let resolutions = 0
const adapter = new class extends LlmAdapter {
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
resolutions += 1
return Promise.resolve({
provider,
id: model,
name: model,
...model === 'capacity'
? { context: { contextWindow: 128_000 } }
: {},
})
}
override stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
if (options.model === 'sync-failure') throw new LlmError('construction failed', 'CONSTRUCTION')
if (options.model === 'async-failure') {
return {
[Symbol.asyncIterator]: () => ({
next: () => Promise.reject(new LlmError('iteration failed', 'ITERATION')),
}),
}
}
return (async function* () {
yield* textResponse(options.model)
})()
}
}()
ctx.llm.registerAdapter(['mock'], adapter)
const agent = ctx.agentLoop.create(SessionId('model-request-live'), {
provider: 'mock',
model: 'capacity',
})
const observed: {
turn: number
step: number
provider: string
model: string
contextWindow?: number
}[] = []
ctx.on('agent/model-request', (subject) => {
if (subject === agent) throw new Error('observer failed')
})
ctx.on('agent/model-request', (subject, turn, step, request) => {
if (subject === agent) observed.push({ turn, step, ...request })
})
ctx.on('agent/request', async (_subject, turn, _step, _signal, next) => ({
...await next(),
model: ['capacity', 'unknown', 'async-failure', 'sync-failure'][turn - 1]!,
}))
for (const prompt of ['one', 'two', 'three', 'four']) {
send(agent, prompt)
await waitForIdle(ctx, agent)
}
expect(observed).toEqual([
{
turn: 1,
step: 1,
provider: 'mock',
model: 'capacity',
contextWindow: 128_000,
},
{
turn: 2,
step: 1,
provider: 'mock',
model: 'unknown',
},
{
turn: 3,
step: 1,
provider: 'mock',
model: 'async-failure',
},
])
expect(resolutions).toBe(4)
})
it('a compaction replace rewrites the resend, and the log explains it', async () => {
const adapter = new MockAdapter([textResponse('one'), textResponse('two')])
const ctx = await harness(adapter)