refactor: apply repository naming contract

Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
Tianyi Cui
2026-08-13 00:36:22 +08:00
parent 101df7cf58
commit a2d0f7f411
3281 changed files with 21730 additions and 21592 deletions

View File

@@ -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: 1fde02dc8c764a189eb78226de4c194bbf0b8a5e
README.zh.md: 711bc92101c4df0abd40375e3d73f993105ea81c
README.md: 1923be694c20149fe5387bd7bb4dac5da32a63a4
README.zh.md: bdce7f2b2fdff0e1542deb3e7fae5c5e7139d13b

View File

@@ -39,7 +39,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. 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`.
`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`; `LlmRuntime` 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.
@@ -60,9 +60,9 @@ The plugin also declares its route in the configurable-provider directory (`ctx.
## App attribution
Every request carries the shared attribution header from dsh-llm's `attributionHeaders()` - the mandatory `User-Agent` baseline identifying the harness (see [dsh-llm § App attribution](../llm/README.md#app-attribution-attributionts)). Direct DeepSeek requests and OpenAI-compatible gateway requests get no provider-specific app-attribution headers under this adapter contract; OpenRouter app attribution is deferred to a future explicit OpenRouter adapter or mode. A request whose `GenerateOptions.purpose` is `compaction` (dsh-compact-basic's auxiliary summarization call) additionally carries `x-deepseek-harness-compact: 1`, so the host can separate compaction traffic from conversation requests.
Every request carries the shared attribution header from dsh-llm's `attributionHeaders()` - the mandatory `User-Agent` baseline identifying the harness (see [dsh-llm § App attribution](../llm/README.md#app-attribution-attributionts)). Direct DeepSeek requests and OpenAI-compatible gateway requests get no provider-specific app-attribution headers under this adapter contract; OpenRouter app attribution is deferred to a future explicit OpenRouter adapter or mode. A request whose `GenerateOptions.purpose` is `compaction` (dsh-compaction-basic's auxiliary summarization call) additionally carries `x-deepseek-harness-compact: 1`, so the host can separate compaction traffic from conversation requests.
DeepSeek request identity is separate from app attribution. After credential resolution, every provider request carries `x-deepseek-harness-user-id` with the stable anonymous id from [`@deepseek-ai/dsh-user-id`](../../session/user-id/README.md); a request carrying `GenerateOptions.sessionId` also sends that exact value as `x-deepseek-harness-session-id`, while a direct call without a session omits the session header. Both headers go to the resolved `baseURL`, including a configured gateway, and remain outside the request body and model-visible content.
DeepSeek request identity is separate from app attribution. After credential resolution, every provider request carries `x-deepseek-harness-user-id` with the stable anonymous id from [`@deepseek-ai/dsh-anonymous-user-id`](../../identity/anonymous-user-id/README.md); a request carrying `GenerateOptions.sessionId` also sends that exact value as `x-deepseek-harness-session-id`, while a direct call without a session omits the session header. Both headers go to the resolved `baseURL`, including a configured gateway, and remain outside the request body and model-visible content.
## Wire-format notes

View File

@@ -39,7 +39,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。Catalog 配置项可以自带 `maxTokens`,它对该模型胜出;不含该上限的配置项以及任何未列出原样传递 id 都解析为 profile 值,因此新增按模型的上限只改变一个模型,而非整条路由。确切模型解析会将胜出值公开为 `defaultMaxTokens``LlmService` 会在 agent loop智能体循环写入 `request/header` 前,将该值填入 `GenerateOptions.maxTokens`,从而仍可根据持久记录重建协议请求。显式的请求值或 `AgentOptions.maxTokens` 值优先,并会序列化为 `max_tokens`。适配器不会根据 `contextWindow` 自动调低该请求预算;上下文或提供方输出上限较小的部署必须配置与其相容的 `maxTokens`
`maxTokens` 是适配器为对话请求配置的输出上限,默认值为 256,000。Catalog 配置项可以自带 `maxTokens`,它对该模型胜出;不含该上限的配置项以及任何未列出原样传递 id 都解析为 profile 值,因此新增按模型的上限只改变一个模型,而非整条路由。确切模型解析会将胜出值公开为 `defaultMaxTokens``LlmRuntime` 会在 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` 失败。
@@ -60,9 +60,9 @@ harness LLM大语言模型seam 的 DeepSeek chat-completions 适配器:
## 应用归因
每个请求都携带 dsh-llm `attributionHeaders()` 的共享归因标头,即用于识别 harness 的必需 `User-Agent` 基线(见 [dsh-llm § 应用归因](../llm/README.md#app-attribution-attributionts)。在该适配器约定adapter contract直接 DeepSeek 请求与 OpenAI 兼容 gateway 请求都不会获得提供方特定应用归因标头OpenRouter 应用归因暂缓到未来的显式 OpenRouter 适配器或模式。`GenerateOptions.purpose``compaction` 的请求dsh-compact-basic 的辅助摘要调用)还会携带 `x-deepseek-harness-compact: 1`,让宿主可以将压缩流量与会话请求分开。
每个请求都携带 dsh-llm `attributionHeaders()` 的共享归因标头,即用于识别 harness 的必需 `User-Agent` 基线(见 [dsh-llm § 应用归因](../llm/README.md#app-attribution-attributionts)。在该适配器约定adapter contract直接 DeepSeek 请求与 OpenAI 兼容 gateway 请求都不会获得提供方特定应用归因标头OpenRouter 应用归因暂缓到未来的显式 OpenRouter 适配器或模式。`GenerateOptions.purpose``compaction` 的请求dsh-compaction-basic 的辅助摘要调用)还会携带 `x-deepseek-harness-compact: 1`,让宿主可以将压缩流量与会话请求分开。
DeepSeek 请求身份独立于应用归因。凭据解析成功后,每个提供方请求都会通过 `x-deepseek-harness-user-id` 携带来自 [`@deepseek-ai/dsh-user-id`](../../session/user-id/README.md) 的稳定匿名 id携带 `GenerateOptions.sessionId` 的请求还会通过 `x-deepseek-harness-session-id` 发送该确切值,缺少会话的直接调用则省略会话标头。两个标头都会发送至解析后的 `baseURL`(包括已配置的 gateway且不会进入请求正文或模型可见内容。
DeepSeek 请求身份独立于应用归因。凭据解析成功后,每个提供方请求都会通过 `x-deepseek-harness-user-id` 携带来自 [`@deepseek-ai/dsh-anonymous-user-id`](../../identity/anonymous-user-id/README.md) 的稳定匿名 id携带 `GenerateOptions.sessionId` 的请求还会通过 `x-deepseek-harness-session-id` 发送该确切值,缺少会话的直接调用则省略会话标头。两个标头都会发送至解析后的 `baseURL`(包括已配置的 gateway且不会进入请求正文或模型可见内容。
## 协议格式说明

View File

@@ -33,12 +33,12 @@
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/dsh-user-id": "workspace:^",
"@deepseek-ai/dsh-anonymous-user-id": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
@@ -47,12 +47,12 @@
},
"devDependencies": {
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/dsh-user-id": "workspace:^",
"@deepseek-ai/dsh-anonymous-user-id": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -19,7 +19,7 @@ import type {
} from '@deepseek-ai/dsh-llm'
import type { CredentialRef } from '@deepseek-ai/dsh-credentials'
import { idleWatchdog, timeoutOf } from '@deepseek-ai/dsh-timeout'
import type { AnonymousUserId } from '@deepseek-ai/dsh-user-id'
import type { AnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
import { serializeRequest } from './serialize.ts'
import type { RequestDefaults } from './serialize.ts'
import { parseSse } from './sse.ts'

View File

@@ -16,10 +16,10 @@ import z from '@deepseek-ai/schemastery'
import { assertUsableApiKey, LlmError, resolveRetryPolicy, RetryPolicySchema } from '@deepseek-ai/dsh-llm'
import type { RetryPolicyConfig } from '@deepseek-ai/dsh-llm'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import { environmentOf, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
import { launchEnvironmentOf, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
import { deepEqualJson, installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/dsh-user-id'
import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
import {
DEFAULT_CONTEXT_WINDOW,
DEFAULT_MAX_TOKENS,
@@ -158,7 +158,7 @@ function resolveModels(models: readonly DeepSeekCatalogModel[] | undefined): Dee
* gateway that checkout is meant to use.
* @returns validated connection facts plus the credential reference.
*/
export function resolveAdapterOptions(config: Config, environment?: EnvironmentSnapshot): ResolvedDeepSeekOptions {
export function resolveAdapterOptions(config: Config, environment?: LaunchEnvironmentSnapshot): ResolvedDeepSeekOptions {
if (config.thinking === 'disabled'
&& config.reasoningEffort !== undefined
&& config.reasoningEffort !== 'off') {
@@ -205,7 +205,7 @@ export function apply(ctx: Context, config: Config): void {
const raw = current()
if (raw === lastRaw && lastGood !== undefined) return lastGood
try {
const next = resolveAdapterOptions(raw, environmentOf(ctx))
const next = resolveAdapterOptions(raw, launchEnvironmentOf(ctx))
lastRaw = raw
lastGood = next
return next
@@ -233,7 +233,7 @@ export function apply(ctx: Context, config: Config): void {
} else {
// Without the seam there is no managed store to rank against, so the
// environment is the whole credential plane.
const ambient = environmentOf(ctx).get(ref)
const ambient = launchEnvironmentOf(ctx).get(ref)
if (ambient !== undefined && ambient.value.length > 0) {
return assertUsableApiKey(ambient.value, 'llm-deepseek', ref)
}

View File

@@ -3,9 +3,9 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, CallId, ReasoningEffortId , createMessage } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, CallId, ReasoningEffortId , createMessage } from '@deepseek-ai/dsh-llm'
import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm'
import { CredentialsLocal } from '@deepseek-ai/dsh-credentials-local'
import { LocalCredentialProvider } from '@deepseek-ai/dsh-credentials-local'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import type { Config } from '@deepseek-ai/dsh-llm-deepseek'
import { assemble, type AssembledResult } from './assemble.ts'
@@ -29,7 +29,7 @@ beforeEach(async () => {
async function harness(_model: string, config: Partial<Config> = {}) {
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, config)
return ctx
}
@@ -78,8 +78,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', ()
vi.stubEnv('DEEPSEEK_API_KEY', '')
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(LlmService)
await ctx.plugin(CredentialsLocal, { path: join(dir, '.credentials.yaml'), watch: false })
await ctx.plugin(LlmRuntime)
await ctx.plugin(LocalCredentialProvider, { path: join(dir, '.credentials.yaml'), watch: false })
await ctx.plugin(LlmDeepSeek, {})
const result = await assemble(ctx, {

View File

@@ -3,8 +3,8 @@ import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import { createEnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
import LlmService, { createUserMessage,
import { createLaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
import LlmRuntime, { createUserMessage,
CONTEXT_WINDOW_EXCEEDED_CODE,
ProviderRequestId,
QUOTA_EXCEEDED_CODE,
@@ -12,7 +12,7 @@ import LlmService, { createUserMessage,
userAgent,
} from '@deepseek-ai/dsh-llm'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/dsh-user-id'
import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
import { SessionId } from '@deepseek-ai/dsh-session'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import { DeepSeekAdapter, resolveAdapterOptions } from '@deepseek-ai/dsh-llm-deepseek'
@@ -41,7 +41,7 @@ async function harness(baseURL: string, config: object = {}) {
// environment, which is the whole credential plane without a mounted seam.
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, { baseURL, ...config })
return ctx
}
@@ -617,7 +617,7 @@ describe('plugin registration and config', () => {
it('registers the deepseek provider and unregisters on dispose (HMR safety)', async () => {
const server = await mockServer([])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const fiber = await ctx.plugin(LlmDeepSeek, {
baseURL: server.url,
})
@@ -635,7 +635,7 @@ describe('plugin registration and config', () => {
it('registers retryPolicy from the provider config', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, {
baseURL: 'http://127.0.0.1:1',
retryPolicy: {
@@ -654,7 +654,7 @@ describe('plugin registration and config', () => {
it('owns the deepseek provider and advertises the default models', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, { baseURL: 'http://127.0.0.1:1' })
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
@@ -681,7 +681,7 @@ describe('plugin registration and config', () => {
it.each(['off', 'max'] as const)('uses the configured %s reasoning default', async (effort) => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, {
baseURL: 'http://127.0.0.1:1',
reasoningEffort: effort,
@@ -701,7 +701,7 @@ describe('plugin registration and config', () => {
it('accepts off as the default when thinking is deployment-disabled', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, {
baseURL: 'http://127.0.0.1:1',
thinking: 'disabled',
@@ -720,7 +720,7 @@ describe('plugin registration and config', () => {
'rejects configured reasoning effort %s when thinking is disabled',
async (reasoningEffort) => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await expect(ctx.plugin(LlmDeepSeek, {
baseURL: 'http://127.0.0.1:1',
thinking: 'disabled',
@@ -750,7 +750,7 @@ describe('plugin registration and config', () => {
it('uses the default model catalog when apply is called directly', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
LlmDeepSeek.apply(ctx, { baseURL: 'http://127.0.0.1:1' })
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
{ provider: 'deepseek-official', id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', inputModalities: ['text'] },
@@ -760,7 +760,7 @@ describe('plugin registration and config', () => {
it('advertises configured models without restricting arbitrary request ids', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, {
baseURL: 'http://127.0.0.1:1',
models: [
@@ -793,7 +793,7 @@ describe('plugin registration and config', () => {
it('uses exact model capacity before the adapter-wide default', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, {
baseURL: 'http://127.0.0.1:1',
defaultContextWindow: 256_000,
@@ -813,7 +813,7 @@ describe('plugin registration and config', () => {
it('allows an explicit empty model catalog', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, {
baseURL: 'http://127.0.0.1:1',
models: [],
@@ -829,7 +829,7 @@ describe('plugin registration and config', () => {
[[{ id: 'm' }, { id: 'm' }], /duplicate catalog model/],
] as const)('rejects invalid advisory model config', async (models, message) => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await expect(ctx.plugin(LlmDeepSeek, {
baseURL: 'http://127.0.0.1:1',
models: [...models],
@@ -859,7 +859,7 @@ describe('plugin registration and config', () => {
it('rejects invalid context capacity when apply is called directly', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
expect(() => {
LlmDeepSeek.apply(ctx, {
baseURL: 'http://127.0.0.1:1',
@@ -876,7 +876,7 @@ describe('plugin registration and config', () => {
.toThrow(/defaultContextWindow must be a positive integer/)
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await expect(ctx.plugin(LlmDeepSeek, {
baseURL: 'http://127.0.0.1:1',
defaultContextWindow,
@@ -892,7 +892,7 @@ describe('plugin registration and config', () => {
.toThrow(/maxTokens must be a positive safe integer/)
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await expect(ctx.plugin(LlmDeepSeek, {
baseURL: 'http://127.0.0.1:1',
maxTokens,
@@ -905,7 +905,7 @@ describe('plugin registration and config', () => {
vi.stubEnv('DEEPSEEK_API_KEY', 'env-key')
vi.stubEnv('DEEPSEEK_BASE_URL', 'http://127.0.0.1:1')
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, {})
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
})
@@ -913,7 +913,7 @@ describe('plugin registration and config', () => {
it('loads keyless, keeps the catalog browsable, and fails the request actionably', async () => {
vi.stubEnv('DEEPSEEK_API_KEY', '')
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, { baseURL: 'http://127.0.0.1:1' })
// First-boot onboarding: the route registers so models stay discoverable;
// only the request itself needs a key.
@@ -937,7 +937,7 @@ describe('plugin registration and config', () => {
vi.stubEnv('DEEPSEEK_API_KEY', 'ambient-key')
const server = await mockServer([{ kind: 'sse', events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, { baseURL: server.url })
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
expect(server.headers[0]?.authorization).toBe('Bearer ambient-key')
@@ -946,7 +946,7 @@ describe('plugin registration and config', () => {
it('treats an empty ambient variable as no key when no credentials seam is mounted', async () => {
vi.stubEnv('DEEPSEEK_API_KEY', '')
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, { baseURL: 'http://127.0.0.1:1' })
const result = await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
expect(result.finish).toMatchObject({ kind: 'error', failure: { code: 'MISSING_CREDENTIAL' } })
@@ -966,7 +966,7 @@ describe('plugin registration and config', () => {
vi.stubEnv('DEEPSEEK_BASE_URL', server.url)
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmDeepSeek, {})
await assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] })
expect(server.requests).toHaveLength(1)
@@ -974,19 +974,19 @@ describe('plugin registration and config', () => {
it('takes DEEPSEEK_BASE_URL from any environment layer, with explicit config still on top', () => {
const trusted = createEnvironmentSnapshot([
const trusted = createLaunchEnvironmentSnapshot([
{ source: 'user-env', path: '/home/.dsh/.env', values: { DEEPSEEK_BASE_URL: 'https://user.example' } },
])
expect(resolveAdapterOptions({}, trusted).baseURL).toBe('https://user.example')
// The product trusts the project it is launched in, so a checkout can
// point its own agent at the gateway that checkout is meant to use.
const project = createEnvironmentSnapshot([
const project = createLaunchEnvironmentSnapshot([
{ source: 'project-env', path: '/work/.env', values: { DEEPSEEK_BASE_URL: 'https://project.example' } },
])
expect(resolveAdapterOptions({}, project).baseURL).toBe('https://project.example')
// An explicitly configured endpoint outranks every environment layer, so a
// stale shell value cannot rewrite a deployment's own gateway.
const shell = createEnvironmentSnapshot([
const shell = createLaunchEnvironmentSnapshot([
{ source: 'process', values: { DEEPSEEK_BASE_URL: 'https://stale.example' } },
])
expect(resolveAdapterOptions({ baseURL: 'https://gateway.internal' }, shell).baseURL).toBe('https://gateway.internal')
@@ -995,7 +995,7 @@ describe('plugin registration and config', () => {
vi.stubEnv('DEEPSEEK_API_KEY', 'k')
vi.stubEnv('DEEPSEEK_BASE_URL', undefined)
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
// Registration succeeds; no call is made (would hit api.deepseek.com).
await ctx.plugin(LlmDeepSeek, {})
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
@@ -1031,7 +1031,7 @@ describe('plugin registration and config', () => {
.toThrow(/streamIdleTimeoutMs.*no greater/)
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await expect(ctx.plugin(LlmDeepSeek, {
baseURL: 'http://127.0.0.1:1',
streamIdleTimeoutMs: 0,
@@ -1044,7 +1044,7 @@ describe('plugin registration and config', () => {
it('rejects invalid nested retryPolicy before registering the provider', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await expect(ctx.plugin(LlmDeepSeek, {
baseURL: 'http://127.0.0.1:1',

View File

@@ -3,11 +3,11 @@ import { Context } from '@deepseek-ai/cordis'
import { access, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import LlmService, { INVALID_CREDENTIAL_CODE } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { INVALID_CREDENTIAL_CODE } from '@deepseek-ai/dsh-llm'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import { CredentialsLocal } from '@deepseek-ai/dsh-credentials-local'
import { LocalCredentialProvider } from '@deepseek-ai/dsh-credentials-local'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { SettingsLocal } from '@deepseek-ai/dsh-settings-local'
import { FileSettingsProvider } from '@deepseek-ai/dsh-settings-file'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import { assemble } from './assemble.ts'
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
@@ -35,7 +35,7 @@ interface Harness {
}
/**
* Real dynamic composition: llm + settings-local + credentials-local +
* Real dynamic composition: llm + settings-file + credentials-local +
* llm-deepseek over one temp harness home. `watch: false` keeps every change
* flowing through the in-process write path, which is deterministic; external
* file watching is the providers' own covered concern.
@@ -46,10 +46,10 @@ async function boot(dir: string, config: object): Promise<Harness> {
cleanups.push(async () => {
await ctx.fiber.dispose()
})
await ctx.plugin(LlmService)
const settingsFiber = ctx.plugin(SettingsLocal, { path: join(dir, 'settings.yaml'), watch: false })
await ctx.plugin(LlmRuntime)
const settingsFiber = ctx.plugin(FileSettingsProvider, { path: join(dir, 'settings.yaml'), watch: false })
await settingsFiber
await ctx.plugin(CredentialsLocal, { path: join(dir, '.credentials.yaml'), watch: false })
await ctx.plugin(LocalCredentialProvider, { path: join(dir, '.credentials.yaml'), watch: false })
await ctx.plugin(LlmDeepSeek, config)
return { ctx, settingsFiber }
}
@@ -87,11 +87,11 @@ describe('request-level dynamic configuration', () => {
const keyless = await prompt(ctx)
expect(keyless.finish).toMatchObject({ kind: 'error', failure: { code: 'MISSING_CREDENTIAL' } })
await expect(access(join(dir, '.userid'))).rejects.toMatchObject({ code: 'ENOENT' })
await expect(access(join(dir, '.anonymous-user-id'))).rejects.toMatchObject({ code: 'ENOENT' })
await ctx.credentials.set(KEY_REF, 'sk-arrived')
await prompt(ctx)
expect(server.headers[0]?.authorization).toBe('Bearer sk-arrived')
await expect(access(join(dir, '.userid'))).resolves.toBeUndefined()
await expect(access(join(dir, '.anonymous-user-id'))).resolves.toBeUndefined()
})
it('rejects a stored credential no header can carry, never echoing it in the failure', async () => {

View File

@@ -1,6 +1,6 @@
/**
* Real-composition guard for the dynamic-configuration chain: LlmService,
* settings-local, credentials-local, and llm-deepseek boot from a test-only
* Real-composition guard for the dynamic-configuration chain: LlmRuntime,
* settings-file, credentials-local, and llm-deepseek boot from a test-only
* cordis.yml through the actual Loader + Include path, external edits of
* settings.yaml and the credentials document hot-publish through their providers, and the very
* next request carries the fresh base URL and credential. The same adapter
@@ -16,12 +16,12 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import LlmService from '@deepseek-ai/dsh-llm'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import CredentialsLocal from '@deepseek-ai/dsh-credentials-local'
import LocalCredentialProvider from '@deepseek-ai/dsh-credentials-local'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import SettingsLocal from '@deepseek-ai/dsh-settings-local'
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-user-id'
import FileSettingsProvider from '@deepseek-ai/dsh-settings-file'
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import { assemble } from './assemble.ts'
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
@@ -63,7 +63,7 @@ async function loadComposition(
...options.withDynamic
? [
'- id: settings',
" name: '@deepseek-ai/dsh-settings-local'",
" name: '@deepseek-ai/dsh-settings-file'",
' config:',
` path: ${JSON.stringify(settingsPath)}`,
' debounceMs: 10',
@@ -87,9 +87,9 @@ async function loadComposition(
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
const modules = new Map<string, unknown>([
['test-llm-service', LlmService],
['@deepseek-ai/dsh-settings-local', SettingsLocal],
['@deepseek-ai/dsh-credentials-local', CredentialsLocal],
['test-llm-service', LlmRuntime],
['@deepseek-ai/dsh-settings-file', FileSettingsProvider],
['@deepseek-ai/dsh-credentials-local', LocalCredentialProvider],
['@deepseek-ai/dsh-llm-deepseek', LlmDeepSeek],
])
ctx.loader.internal = {

View File

@@ -24,19 +24,19 @@
"path": "../../credentials/credentials"
},
{
"path": "../../util/environment"
"path": "../../util/launch-environment"
},
{
"path": "../../settings/settings"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../util/timeout"
},
{
"path": "../../session/user-id"
"path": "../../identity/anonymous-user-id"
}
]
}

View File

@@ -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-pi-ai/README.md
README.md: c68db532e94c1b0e956d933fbdcf51e5bb63dd29
README.zh.md: a621fa67c58c6b185e1330668cb8fac05f17d157
README.md: e6d04133f3dfa4eb4cd9aa997ad1f9bb93fa50a0
README.zh.md: e33bcac7081006573f5c6a8a8a3221fa57839c4f

View File

@@ -137,7 +137,7 @@ Credentials never enter that collection. The harness resolves a route's key thro
The selected model descriptor supplies the protocol implementation. This includes native API differences such as OpenAI models whose descriptor uses the Responses API rather than Chat Completions; the harness adapter does not hardcode endpoint selection by model name.
Successful assistant responses store a versioned, lossless-JSON replay state beside the provider and model that produced them. At request time, `LlmService` passes replay state only when the historical provider route and target provider route are currently owned by this same `PiAiAdapter` instance. The adapter validates the state and restores pi-ai response ids and provider signatures even when the target provider or model changes; pi-ai then decides which metadata its target API can reuse. History without replay state is translated as foreign provider-neutral content and never impersonates a native pi-ai response.
Successful assistant responses store a versioned, lossless-JSON replay state beside the provider and model that produced them. At request time, `LlmRuntime` passes replay state only when the historical provider route and target provider route are currently owned by this same `PiAiAdapter` instance. The adapter validates the state and restores pi-ai response ids and provider signatures even when the target provider or model changes; pi-ai then decides which metadata its target API can reuse. History without replay state is translated as foreign provider-neutral content and never impersonates a native pi-ai response.
If a listener rewrites assembled assistant content, the loop drops replay state before logging the message because its provider metadata no longer describes the content. Invalid versions, malformed metadata, provider/model mismatches between the message and replay state, and content/block mismatches fail explicitly with `LlmError('INVALID_REPLAY_STATE')`.

View File

@@ -138,7 +138,7 @@ profile 的 `models` 列表是*替换*该路由已安装 catalog而不是扩
所选模型 descriptor 提供协议实现。这包括原生 API 差异,例如 descriptor 使用 Responses API 而非 Chat Completions 的 OpenAI 模型harness 适配器不会按模型名称硬编码端点选择。
成功的 assistant 响应会将经版本化的无损 JSON 回放状态与生成该响应的提供方和模型一同存储。请求时,`LlmService` 只有在历史提供方路由与目标提供方路由当前由同一个 `PiAiAdapter` 实例拥有时,才会传递回放状态。即使目标提供方或模型改变,适配器也会验证状态并恢复 pi-ai 响应 id 与提供方 signature随后由 pi-ai 判定目标 API 可以复用哪些元数据。没有回放状态的历史会被转换为外来的、与提供方无关的内容,绝不伪装为原生 pi-ai 响应。
成功的 assistant 响应会将经版本化的无损 JSON 回放状态与生成该响应的提供方和模型一同存储。请求时,`LlmRuntime` 只有在历史提供方路由与目标提供方路由当前由同一个 `PiAiAdapter` 实例拥有时,才会传递回放状态。即使目标提供方或模型改变,适配器也会验证状态并恢复 pi-ai 响应 id 与提供方 signature随后由 pi-ai 判定目标 API 可以复用哪些元数据。没有回放状态的历史会被转换为外来的、与提供方无关的内容,绝不伪装为原生 pi-ai 响应。
如果 listener 改写已组装 assistant 内容loop 会在记录消息前丢弃回放状态,因为其提供方元数据不再描述该内容。无效版本、格式错误元数据、消息与回放状态之间的提供方/模型不匹配,以及内容/块不匹配都会显式以 `LlmError('INVALID_REPLAY_STATE')` 失败。

View File

@@ -34,7 +34,7 @@
"peerDependencies": {
"@deepseek-ai/dsh-attachment": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
@@ -48,7 +48,7 @@
"devDependencies": {
"@deepseek-ai/dsh-attachment": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-environment": "workspace:^",
"@deepseek-ai/dsh-launch-environment": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",

View File

@@ -56,7 +56,7 @@
*/
import type { Context } from '@deepseek-ai/cordis'
import { environmentOf } from '@deepseek-ai/dsh-environment'
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
import { assertUsableApiKey, LlmError } from '@deepseek-ai/dsh-llm'
import type { AdapterRegistrationHandle, DirectoryRegistrationHandle, LlmConfigurableProvider } from '@deepseek-ai/dsh-llm'
import { deepEqualJson, installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
@@ -175,7 +175,7 @@ export function apply(ctx: Context, config: Config): void {
const hit = credentials !== undefined
? (await credentials.resolve(ref))?.value
// Without the seam the environment is the whole credential plane.
: environmentOf(ctx).get(ref)?.value
: launchEnvironmentOf(ctx).get(ref)?.value
if (hit !== undefined && hit.length > 0) return assertUsableApiKey(hit, 'llm-pi-ai', ref)
throw new LlmError(
`llm-pi-ai: no credential for provider route "${provider}"; its profile resolves ${ref}, which is not`

View File

@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, CallId, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, CallId, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm'
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
import type { PiAiProviderProfile } from '@deepseek-ai/dsh-llm-pi-ai'
@@ -21,7 +21,7 @@ const contexts: Context[] = []
async function harness(_model: string, config: Partial<PiAiProviderProfile> = {}) {
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {
providers: {
deepseek: {
@@ -148,7 +148,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-pi-ai e2e (real API)', () =>
// cross-implementation check that the StreamChunk design holds.
const deepseekCtx = new Context()
contexts.push(deepseekCtx)
await deepseekCtx.plugin(LlmService)
await deepseekCtx.plugin(LlmRuntime)
await deepseekCtx.plugin(LlmDeepSeek, { thinking: 'disabled' })
const piCtx = await harness(FLASH)

View File

@@ -7,7 +7,7 @@ import type {
SaveImageAttachment,
StoredImageAttachment,
} from '@deepseek-ai/dsh-attachment'
import LlmService, { createUserMessage, CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, ReasoningEffortId, userAgent } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, ReasoningEffortId, userAgent } from '@deepseek-ai/dsh-llm'
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
@@ -32,7 +32,7 @@ const IMAGE_REF: ImageAttachmentRef = {
async function harness(baseURL: string, overrides: Record<string, unknown> = {}): Promise<Context> {
vi.stubEnv('PI_TEST_KEY', 'test-key')
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {
providers: { deepseek: { apiKeyEnv: 'PI_TEST_KEY', baseURL, ...overrides } },
})
@@ -144,7 +144,7 @@ describe('PiAiAdapter provider routing', () => {
it('preserves omitted profile options when constructing the adapter directly', async () => {
const server = await mockServer([{ events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['deepseek'], adapterOf({
deepseek: { apiKeyEnv: 'PI_TEST_KEY', baseURL: server.url },
}))
@@ -189,7 +189,7 @@ describe('PiAiAdapter provider routing', () => {
it('uses the catalog API implementation, including OpenAI Responses', async () => {
const server = await mockServer([{ status: 401, body: JSON.stringify({ error: { message: 'expected mock failure' } }) }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {
providers: { openai: { apiKeyEnv: 'PI_TEST_KEY', baseURL: `${server.url}/v1` } },
})
@@ -234,7 +234,7 @@ describe('PiAiAdapter provider routing', () => {
}
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {
providers: { openai: { apiKeyEnv: 'PI_TEST_KEY', baseURL: `${server.url}/v1` } },
})
@@ -265,7 +265,7 @@ describe('PiAiAdapter provider routing', () => {
{ status: 500, body: JSON.stringify({ error: { message: 'second hidden SDK retry' } }) },
])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {
providers: { openai: { apiKeyEnv: 'PI_TEST_KEY', baseURL: `${server.url}/v1` } },
})
@@ -279,7 +279,7 @@ describe('PiAiAdapter provider routing', () => {
it('uses OpenAI Responses against an Azure project v1 path with its API key header', async () => {
const server = await mockServer([{ status: 401, body: JSON.stringify({ error: { message: 'expected mock failure' } }) }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {
providers: {
openai: {
@@ -367,7 +367,7 @@ describe('provider profile lifecycle', () => {
it('registers every profile atomically and unregisters on dispose', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const fiber = await ctx.plugin(LlmPiAi, {
providers: {
openai: {
@@ -399,7 +399,7 @@ describe('provider profile lifecycle', () => {
it('exposes the installed pi-ai model catalog through provider-neutral metadata', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, { providers: { openai: {} } })
const models = await ctx.llm.listModels('openai')
expect(models.find(model => model.id === 'gpt-4.1')).toEqual({
@@ -413,7 +413,7 @@ describe('provider profile lifecycle', () => {
it('exposes pi-ai model thinking levels verbatim without inventing a provider default', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {
providers: { deepseek: {}, openai: {} },
})
@@ -446,7 +446,7 @@ describe('provider profile lifecycle', () => {
it('uses a supported profile reasoning value as the model default and rejects an unsupported one', async () => {
const supported = new Context()
await supported.plugin(LlmService)
await supported.plugin(LlmRuntime)
await supported.plugin(LlmPiAi, {
providers: { deepseek: { reasoning: 'max' } },
})
@@ -459,7 +459,7 @@ describe('provider profile lifecycle', () => {
// field would hide every model on the route, including the ones that do
// support the level. The request path below is where it is refused.
const unsupported = new Context()
await unsupported.plugin(LlmService)
await unsupported.plugin(LlmRuntime)
await unsupported.plugin(LlmPiAi, {
providers: { deepseek: { reasoning: 'medium' } },
})
@@ -473,7 +473,7 @@ describe('provider profile lifecycle', () => {
})
const disabled = new Context()
await disabled.plugin(LlmService)
await disabled.plugin(LlmRuntime)
await disabled.plugin(LlmPiAi, {
providers: { deepseek: { reasoning: 'off' } },
})
@@ -483,7 +483,7 @@ describe('provider profile lifecycle', () => {
it('serves declared reasoning efforts to selectors and honours the profile default', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {
providers: {
'acme-gateway': {
@@ -519,7 +519,7 @@ describe('provider profile lifecycle', () => {
vi.stubEnv('PI_TEST_KEY', 'test-key')
const server = await mockServer([{ events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {
providers: {
'acme-gateway': {
@@ -562,7 +562,7 @@ describe('provider profile lifecycle', () => {
vi.stubEnv('PI_TEST_KEY', 'test-key')
const server = await mockServer([{ events: textEvents }, { events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {
providers: {
'acme-gateway': {
@@ -600,7 +600,7 @@ describe('provider profile lifecycle', () => {
vi.stubEnv('PI_TEST_KEY', 'test-key')
const server = await mockServer([{ events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {
providers: {
'acme-gateway': {
@@ -633,7 +633,7 @@ describe('provider profile lifecycle', () => {
vi.stubEnv('PI_TEST_KEY', 'test-key')
const server = await mockServer([{ events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {
providers: {
'acme-gateway': {
@@ -717,7 +717,7 @@ describe('provider profile lifecycle', () => {
const legacy = { [field]: 2 }
expect(() => resolveProfiles({ openai: legacy })).toThrow(/removed.*agent recovery/i)
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await expect(ctx.plugin(LlmPiAi, { providers: { openai: legacy } }))
.rejects.toThrow(/removed.*agent recovery/i)
},
@@ -733,7 +733,7 @@ describe('provider profile lifecycle', () => {
]
for (const entry of invalid) {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await expect(ctx.plugin(LlmPiAi, { providers: { openai: { ...entry } } }))
.rejects.toThrow()
}
@@ -745,7 +745,7 @@ describe('provider profile lifecycle', () => {
})).toThrow(/retryPolicy\.backoff\.jitterRatio/)
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await expect(ctx.plugin(LlmPiAi, {
providers: { openai: { retryPolicy: { mode: 'normal', maxRetries: -1 } } },
})).rejects.toThrow(/retryPolicy/)

View File

@@ -3,9 +3,9 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { StreamChunk } from '@deepseek-ai/dsh-llm'
import SettingsLocal from '@deepseek-ai/dsh-settings-local'
import FileSettingsProvider from '@deepseek-ai/dsh-settings-file'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
@@ -45,8 +45,8 @@ async function home(): Promise<string> {
/** The dormant composition plus a real settings service, as the product mounts it. */
async function bootWithSettings(dir: string, config: LlmPiAi.Config): Promise<Context> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(SettingsLocal, { path: join(dir, 'settings.yaml'), watch: false })
await ctx.plugin(LlmRuntime)
await ctx.plugin(FileSettingsProvider, { path: join(dir, 'settings.yaml'), watch: false })
await ctx.plugin(LlmPiAi, config)
return ctx
}
@@ -69,7 +69,7 @@ function gateway(baseURL: string, overrides: Record<string, unknown> = {}): LlmP
async function harness(config: LlmPiAi.Config): Promise<Context> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, config)
return ctx
}

View File

@@ -2,7 +2,7 @@ import { createServer } from 'node:http'
import type { IncomingMessage, Server, ServerResponse } from 'node:http'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { userAgent } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { userAgent } from '@deepseek-ai/dsh-llm'
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
import { getBuiltinModels } from '@earendil-works/pi-ai/providers/all'
import { discoverModels } from '../src/discovery.ts'
@@ -67,7 +67,7 @@ async function listingServer(behavior: {
/** A bare dormant mount: discovery is offered whether or not a route exists. */
async function harness(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {})
return ctx
}
@@ -153,7 +153,7 @@ describe('draft-provider model discovery', () => {
// and read as a wrong key.
const server = await listingServer({ body: JSON.stringify({ data: [{ id: 'm' }] }) })
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
process.env['ACME_GATEWAY_KEY'] = 'stored-key'
touchedEnv.push('ACME_GATEWAY_KEY')
await ctx.plugin(LlmPiAi, {
@@ -183,7 +183,7 @@ describe('draft-provider model discovery', () => {
// profile names a credential that is not set must still answer rather than
// failing over a key the interrogation never needed.
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
Reflect.deleteProperty(process.env, 'ABSENT_FOR_DISCOVERY')
await ctx.plugin(LlmPiAi, { providers: { deepseek: { apiKeyEnv: 'ABSENT_FOR_DISCOVERY' } } })
@@ -322,7 +322,7 @@ describe('draft-provider model discovery', () => {
it('withdraws the offer when the plugin unloads', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const fiber = await ctx.plugin(LlmPiAi, {})
await expect(ctx.llm.discoverModels('llm-pi-ai', { provider: 'openai' })).resolves.not.toHaveLength(0)

View File

@@ -3,11 +3,11 @@ import { Context } from '@deepseek-ai/cordis'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import LlmService, { LlmAdapter } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { LlmAdapter } from '@deepseek-ai/dsh-llm'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import { CredentialsLocal } from '@deepseek-ai/dsh-credentials-local'
import { LocalCredentialProvider } from '@deepseek-ai/dsh-credentials-local'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { SettingsLocal } from '@deepseek-ai/dsh-settings-local'
import { FileSettingsProvider } from '@deepseek-ai/dsh-settings-file'
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
import { assemble } from './assemble.ts'
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
@@ -42,9 +42,9 @@ async function boot(dir: string, config: LlmPiAi.Config): Promise<Context> {
cleanups.push(async () => {
await ctx.fiber.dispose()
})
await ctx.plugin(LlmService)
await ctx.plugin(SettingsLocal, { path: join(dir, 'settings.yaml'), watch: false })
await ctx.plugin(CredentialsLocal, { path: join(dir, '.credentials.yaml'), watch: false })
await ctx.plugin(LlmRuntime)
await ctx.plugin(FileSettingsProvider, { path: join(dir, 'settings.yaml'), watch: false })
await ctx.plugin(LocalCredentialProvider, { path: join(dir, '.credentials.yaml'), watch: false })
await ctx.plugin(LlmPiAi, config)
return ctx
}

View File

@@ -1,6 +1,6 @@
/**
* Real-composition guard for the dormant pi-ai posture: LlmService,
* settings-local, credentials-local, and a bare `llm-pi-ai` row boot from a
* Real-composition guard for the dormant pi-ai posture: LlmRuntime,
* settings-file, credentials-local, and a bare `llm-pi-ai` row boot from a
* test-only cordis.yml through the actual Loader + Include path, an external
* edit of settings.yaml registers the route live, and the next request
* carries the credential the credentials document supplies. A hand-mounted `ctx.plugin` cannot
@@ -16,9 +16,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import LlmService from '@deepseek-ai/dsh-llm'
import CredentialsLocal from '@deepseek-ai/dsh-credentials-local'
import SettingsLocal from '@deepseek-ai/dsh-settings-local'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import LocalCredentialProvider from '@deepseek-ai/dsh-credentials-local'
import FileSettingsProvider from '@deepseek-ai/dsh-settings-file'
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
import { assemble } from './assemble.ts'
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
@@ -47,7 +47,7 @@ async function loadComposition(): Promise<{ ctx: Context; settingsPath: string }
'- id: llm',
" name: 'test-llm-service'",
'- id: settings',
" name: '@deepseek-ai/dsh-settings-local'",
" name: '@deepseek-ai/dsh-settings-file'",
' config:',
` path: ${JSON.stringify(settingsPath)}`,
' debounceMs: 10',
@@ -67,9 +67,9 @@ async function loadComposition(): Promise<{ ctx: Context; settingsPath: string }
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
const modules = new Map<string, unknown>([
['test-llm-service', LlmService],
['@deepseek-ai/dsh-settings-local', SettingsLocal],
['@deepseek-ai/dsh-credentials-local', CredentialsLocal],
['test-llm-service', LlmRuntime],
['@deepseek-ai/dsh-settings-file', FileSettingsProvider],
['@deepseek-ai/dsh-credentials-local', LocalCredentialProvider],
['@deepseek-ai/dsh-llm-pi-ai', LlmPiAi],
])
ctx.loader.internal = {

View File

@@ -8,7 +8,7 @@ import type {
SaveImageAttachment,
StoredImageAttachment,
} from '@deepseek-ai/dsh-attachment'
import LlmService, { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm'
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
import type { PiAiReplayState } from '../src/replay.ts'
@@ -54,7 +54,7 @@ const contexts: Context[] = []
async function harness(image?: StoredImageAttachment): Promise<Context> {
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(LlmPiAi, {
providers: Object.fromEntries(providerCases.map(profile => [profile.provider, {
...profile.apiKey === undefined ? {} : { apiKey: profile.apiKey },

View File

@@ -9,7 +9,7 @@
],
"references": [
{
"path": "../../util/environment"
"path": "../../util/launch-environment"
},
{
"path": "../../../vendor/cosmokit"
@@ -33,7 +33,7 @@
"path": "../../settings/settings"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../util/timeout"

View File

@@ -3,7 +3,7 @@ import { Context } from '@deepseek-ai/cordis'
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
import { createUserMessage, ProviderRequestId } from '@deepseek-ai/dsh-llm'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as RetryInvariant from '@deepseek-ai/dsh-llm-retry/invariant'
import { RetryId } from '@deepseek-ai/dsh-llm-retry'
import { providerForOpenStep } from '../src/history.ts'
@@ -11,7 +11,7 @@ import { providerForOpenStep } from '../src/history.ts'
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(RetryInvariant)
return ctx
}
@@ -299,7 +299,7 @@ describe('llm-retry invariants', () => {
})
appendRetryTurn(missingStart, 2)
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await expect(ctx.plugin(RetryInvariant)).resolves.toBeDefined()
})
@@ -317,7 +317,7 @@ describe('llm-retry invariants', () => {
const session = ctx.sessions.create(SessionId('retry-invariant-late'))
session.append('step/start', { turn: 1, step: 1 })
session.append('llm/retry', { turn: 1, step: 1, ...normal })
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await expect(ctx.plugin(RetryInvariant)).rejects.toThrow(/inside an open turn/)
})
@@ -329,7 +329,7 @@ describe('llm-retry invariants', () => {
session.append('llm/retry-started', {
retryId: normal.retryId, turn: 1, step: 1, retry: 1,
})
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await expect(ctx.plugin(RetryInvariant)).resolves.toBeDefined()
})
})

View File

@@ -8,11 +8,11 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import LlmService, { createUserMessage, LlmAdapter, LlmError, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, LlmAdapter, LlmError, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, ResolvedRetryPolicy, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import * as retry from '../src/index.ts'
let root: string | undefined
@@ -58,10 +58,10 @@ async function loadYaml(lines: readonly string[]): Promise<Context> {
await context.plugin(Loader)
context.loader.builtins.include = Include
const modules = new Map<string, unknown>([
['@deepseek-ai/dsh-llm', LlmService],
['@deepseek-ai/dsh-llm', LlmRuntime],
['@deepseek-ai/dsh-session', SessionStore],
['@deepseek-ai/dsh-system-prompt', SystemPrompt],
['@deepseek-ai/dsh-tools', ToolRegistry],
['@deepseek-ai/dsh-tools', ToolRuntime],
['@deepseek-ai/dsh-agent', AgentRegistry],
['@deepseek-ai/dsh-llm-retry', retry],
['@deepseek-ai/dsh-agent-loop', AgentLoop],

View File

@@ -4,8 +4,8 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionPersistenceSqlite from '@deepseek-ai/dsh-session-persistence-sqlite'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SqliteSessionPersistence from '@deepseek-ai/dsh-session-persistence-sqlite'
import { RetryId } from '@deepseek-ai/dsh-llm-retry'
import type {} from '../src/index.ts'
@@ -21,9 +21,9 @@ async function backend(kind: 'jsonl' | 'sqlite'): Promise<Context> {
if (kind === 'jsonl') {
const root = await mkdtemp(join(tmpdir(), 'dsh-llm-retry-jsonl-'))
dirs.push(root)
await ctx.plugin(SessionPersistenceJsonl, { root })
await ctx.plugin(JsonlSessionPersistence, { root })
} else {
await ctx.plugin(SessionPersistenceSqlite, { path: ':memory:' })
await ctx.plugin(SqliteSessionPersistence, { path: ':memory:' })
}
return ctx
}

View File

@@ -1,7 +1,7 @@
import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import type { Fiber } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
import type {
AlwaysRetryPolicyConfig,
BackoffConfig,
@@ -15,7 +15,7 @@ import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionEventMap } from '@deepseek-ai/dsh-session'
import type { LlmRetryEventData } from '@deepseek-ai/dsh-llm-retry/types'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent, RequestErrorAction } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
@@ -105,10 +105,10 @@ async function harness(
internals: retry.RetryInternals = {},
): Promise<{ ctx: Context; retryFiber: Fiber; disposeAdapter: () => void }> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
beforeRetry?.(ctx)
adapter.configureRetryPolicies(policies)

View File

@@ -30,7 +30,7 @@
"path": "../../core/agent"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../util/timeout"

View File

@@ -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/README.md
README.md: a5aa3155c57ae30d58ced3c79a3db903c675fd7e
README.zh.md: 0a920152e48e75cc70611fe58edebfdf7bed2c7b
README.md: 2cae9a05a58295b06d25382729a3304dbdc9a6fa
README.zh.md: bf86e703fc15231c6845364dc383b7fc6ad77297

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
Provider-neutral LLM vocabulary and abstract service. This package defines the canonical language spoken by the agent loop, session logs, and every plugin.
## Service: `LlmService` (ctx key: `llm`)
## Service: `LlmRuntime` (ctx key: `llm`)
An adapter registry plus a single streaming call API, interceptable via a waterfall event.
@@ -24,7 +24,7 @@ An adapter registry plus a single streaming call API, interceptable via a waterf
- `ctx.llm.prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>` Resolve a config plus detached context metadata and markers for fields supplied by adapter defaults in one exact-model lookup, then capture its current adapter registration and immutable retry policy as one cancellable, one-shot call.
- `ctx.llm.stream(options: GenerateOptions): AsyncIterable<StreamChunk>` Stream one model call as raw chunks (token-level deltas). Consumers assemble the chunks into blocks/messages with `BlockAssembler`.
`LlmService` normalizes failures from final adapter selection, synchronous dispatch, iterator construction, and iteration into the stream protocol's single terminal form: `finish { kind: 'error' | 'aborted', failure }`. A failure after partial deltas may leave content blocks open; consumers discard that incomplete output. Errors from `llm/stream` middleware, nested calls, adapter cleanup, and downstream consumers remain thrown because they are plugin or consumer failures rather than model-request outcomes. A prepared call exposes the immutable retry policy captured with its exact adapter registration; a route handled entirely by middleware has no serving policy.
`LlmRuntime` normalizes failures from final adapter selection, synchronous dispatch, iterator construction, and iteration into the stream protocol's single terminal form: `finish { kind: 'error' | 'aborted', failure }`. A failure after partial deltas may leave content blocks open; consumers discard that incomplete output. Errors from `llm/stream` middleware, nested calls, adapter cleanup, and downstream consumers remain thrown because they are plugin or consumer failures rather than model-request outcomes. A prepared call exposes the immutable retry policy captured with its exact adapter registration; a route handled entirely by middleware has no serving policy.
Interrogating an endpoint is configuration-time work over a *draft*, keyed by settings namespace rather than by provider route — the provider a surface is adding does not exist yet, so there is no route to name. The request may still *name* a route it is editing, and an adapter that already describes that route answers from its own knowledge without a network call; `baseURL` is optional and one of the two is required. The request otherwise carries the endpoint, the protocol, and a credential the harness uses for that one interrogation and never stores — nothing here reads or writes settings or credentials, and the reply is candidate metadata a surface may offer for adoption, never a registered catalog. `LlmDiscoveredModel` makes every field but `id` optional because most provider listings disclose an id and nothing else; a surface adopting one still owes the capacities its adapter requires. Duplicate and unusable ids are dropped, an unserved namespace fails with `NO_DISCOVERY`, and a request naming neither a route nor an endpoint fails with `INVALID_DISCOVERY`.
@@ -51,7 +51,7 @@ Exact-model metadata is a separate correctness query, not a catalog decoration o
`Message` is the shared immutable value used by delivery, durable history, and model requests. Every message has a required `MessageId`, role, content, and typed source from creation onward. `createMessage(input)` mints the identity and returns a detached deep-frozen value; `createUserMessage({ content, source })` fixes the user role; `createAssistantMessage({ content, source })` fixes the assistant role and model source kind; `createToolResultMessage({ callId, content, isError })` fixes the user role and couples the tool source to its result block; `freezeMessage(message)` imports an identity that already exists and never replaces it. Message rewrites preserve the identity and produce another frozen value. Browser code imports these value constructors from the dependency-minimal `@deepseek-ai/dsh-llm/message` entry instead of the service-bearing package root.
Message content is an array of typed blocks: `text`, `reasoning`, `tool-call`, `tool-result`. The union is derived from the merge-extensible `ContentBlockMap`, so plugins can add block types via declaration merging. Assistant messages use a model source carrying the provider and model that produced them plus optional adapter-private replay state. Before dispatch, `LlmService` retains that state only when the historical provider route and target provider route are currently owned by the exact same adapter instance; the adapter then decides whether it can restore or convert the state across models/providers. The core block set is limited to blocks every shipping path honors — multimodal content (images, audio, …) has no core block type; a feature that needs one adds it via the map together with the adapter/UI/compaction support that honors it.
Message content is an array of typed blocks: `text`, `reasoning`, `tool-call`, `tool-result`. The union is derived from the merge-extensible `ContentBlockMap`, so plugins can add block types via declaration merging. Assistant messages use a model source carrying the provider and model that produced them plus optional adapter-private replay state. Before dispatch, `LlmRuntime` retains that state only when the historical provider route and target provider route are currently owned by the exact same adapter instance; the adapter then decides whether it can restore or convert the state across models/providers. The core block set is limited to blocks every shipping path honors — multimodal content (images, audio, …) has no core block type; a feature that needs one adds it via the map together with the adapter/UI/compaction support that honors it.
Streaming is a raw chunk protocol (`block-start`, `text-delta`, `reasoning-delta`, `tool-call-delta`, `block-end`, `usage`, `finish`). Every adapter outcome reaches consumers as one terminal `finish`; operational failure uses its `error` or `aborted` reason rather than throwing across the stream API. `BlockAssembler` is the single shared implementation that assembles chunks into blocks/messages.
@@ -81,7 +81,7 @@ Every adapter that puts a credential in an HTTP header judges it the same way be
### Real adapters
Two adapters implement `LlmAdapter` on different internals: [`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) uses direct fetch with `eventsource-parser` SSE framing for the `deepseek-official` route, while [`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) dynamically resolves configured provider/model pairs through `@earendil-works/pi-ai`. Both follow the `StreamChunk` conventions in `types.ts`: usage precedes finish and tool arguments remain raw strings. Adapter implementations may throw or emit a failure finish internally; `LlmService` exposes both as a terminal failure finish. See [the twin LLM adapters](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the adapter rationale and [the terminal-failure decision](../../../.agents/notes/implemented/architecture/2026-07-29-terminal-llm-stream-failures.md) for the service boundary.
Two adapters implement `LlmAdapter` on different internals: [`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) uses direct fetch with `eventsource-parser` SSE framing for the `deepseek-official` route, while [`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) dynamically resolves configured provider/model pairs through `@earendil-works/pi-ai`. Both follow the `StreamChunk` conventions in `types.ts`: usage precedes finish and tool arguments remain raw strings. Adapter implementations may throw or emit a failure finish internally; `LlmRuntime` exposes both as a terminal failure finish. See [the twin LLM adapters](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the adapter rationale and [the terminal-failure decision](../../../.agents/notes/implemented/architecture/2026-07-29-terminal-llm-stream-failures.md) for the service boundary.
## Model Experience

View File

@@ -4,7 +4,7 @@
提供方无关的 LLM大语言模型词汇与抽象服务。本包定义 agent loop智能体循环、会话日志和每个插件使用的规范语言。
## 服务:`LlmService`ctx key`llm`
## 服务:`LlmRuntime`ctx key`llm`
一个适配器注册表加单一流式调用接口,可通过 waterfall瀑布式事件拦截。
@@ -24,7 +24,7 @@
- `ctx.llm.prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>` 在一次精确模型查询中解析配置、脱耦的上下文元数据以及标明哪些字段由适配器默认值填入的标记,再将当前适配器注册和不可变重试策略捕获为一次可取消、一次性调用。
- `ctx.llm.stream(options: GenerateOptions): AsyncIterable<StreamChunk>` 将一次模型调用流式输出为原始分片token 级增量)。消费方使用 `BlockAssembler` 将分片组装为块/消息。
`LlmService` 将最终适配器选择、同步 dispatch、iterator 构造与迭代中的失败规范化为流协议唯一的终止形式:`finish { kind: 'error' | 'aborted', failure }`。部分增量输出后发生失败时,内容块可能仍未闭合;消费方会丢弃这些不完整输出。`llm/stream` middleware、嵌套调用、适配器清理和下游消费方的错误仍会抛出因为它们属于插件或消费方失败而非模型请求结果。已准备调用会暴露随其确切适配器注册一同捕获的不可变重试策略完全由 middleware 处理的路由没有服务策略。
`LlmRuntime` 将最终适配器选择、同步 dispatch、iterator 构造与迭代中的失败规范化为流协议唯一的终止形式:`finish { kind: 'error' | 'aborted', failure }`。部分增量输出后发生失败时,内容块可能仍未闭合;消费方会丢弃这些不完整输出。`llm/stream` middleware、嵌套调用、适配器清理和下游消费方的错误仍会抛出因为它们属于插件或消费方失败而非模型请求结果。已准备调用会暴露随其确切适配器注册一同捕获的不可变重试策略完全由 middleware 处理的路由没有服务策略。
询问端点属于配置期针对**草稿**的操作,以 settings namespace 而非提供方路由为键——界面正在新增的提供方还不存在,也就没有路由可点名。但请求仍可**点名**它正在编辑的路由,而已经描述该路由的适配器会用自己的知识作答,无需联网;`baseURL` 可选,两者至少要有一个。除此之外,请求携带端点、协议,以及一条 harness 只用于这一次询问、绝不存储的凭据——这里既不读也不写 settings 与 credentials回复是界面可供用户采纳的候选元数据而不是已注册的 catalog。`LlmDiscoveredModel``id` 外每个字段都是可选的,因为大多数提供方列表只公布 id采纳其中一条的界面仍要补上其适配器所需的容量。重复与不可用的 id 会被丢弃,无人服务的 namespace 以 `NO_DISCOVERY` 失败,既不点名路由也不给端点的请求以 `INVALID_DISCOVERY` 失败。
@@ -51,7 +51,7 @@
`Message` 是投递、持久历史和模型请求共享的不可变值。每条消息从创建起都必须具有 `MessageId`、角色、内容和带类型的来源。`createMessage(input)` 生成标识,并返回与输入分离且深度冻结的值;`createUserMessage({ content, source })` 固定 user 角色;`createAssistantMessage({ content, source })` 固定 assistant 角色与模型来源类别;`createToolResultMessage({ callId, content, isError })` 固定 user 角色,并将工具来源与其结果块耦合;`freezeMessage(message)` 导入已有标识,绝不将其替换。改写消息时会保留标识,并产生另一个冻结值。浏览器端代码会从依赖最少的 `@deepseek-ai/dsh-llm/message` 入口导入这些值构造函数,而不是从包含服务的包根入口导入。
消息内容是类型化内容块数组:`text``reasoning``tool-call``tool-result`。联合从可合并扩展的 `ContentBlockMap` 派生,因此插件可以通过 declaration merging 添加块类型。assistant 消息使用模型来源其中携带生成该消息的提供方和模型以及可选的适配器私有回放状态。dispatch 前,`LlmService` 只在历史提供方路由与目标提供方路由当前由完全相同的适配器实例拥有时才保留该状态;随后由适配器判定能否在模型/提供方间恢复或转换该状态。核心块集只包含每条已发布路径都支持的块。多模态内容(图像、音频等)没有核心块类型;需要它的功能会通过 map 添加并一并添加相应的适配器UI压缩compaction支持。
消息内容是类型化内容块数组:`text``reasoning``tool-call``tool-result`。联合从可合并扩展的 `ContentBlockMap` 派生,因此插件可以通过 declaration merging 添加块类型。assistant 消息使用模型来源其中携带生成该消息的提供方和模型以及可选的适配器私有回放状态。dispatch 前,`LlmRuntime` 只在历史提供方路由与目标提供方路由当前由完全相同的适配器实例拥有时才保留该状态;随后由适配器判定能否在模型/提供方间恢复或转换该状态。核心块集只包含每条已发布路径都支持的块。多模态内容(图像、音频等)没有核心块类型;需要它的功能会通过 map 添加并一并添加相应的适配器UI压缩compaction支持。
流式输出是原始分片协议(`block-start``text-delta``reasoning-delta``tool-call-delta``block-end``usage``finish`)。每个适配器结果都以一个终止 `finish` 到达消费方;运行故障使用 `error``aborted` 作为结束原因,而不会跨流 API 抛出。`BlockAssembler` 是将分片组装为块/消息的唯一共享实现。
@@ -81,7 +81,7 @@
### 真实适配器
两个适配器使用不同内部机制实现 `LlmAdapter`[`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) 针对 `deepseek-official` 路由使用直接 fetch 加 `eventsource-parser` SSEServer-Sent Events分帧[`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) 则通过 `@earendil-works/pi-ai` 动态解析已配置提供方/模型对。两者都遵循 `types.ts` 中的 `StreamChunk` 约定usage 先于 finish工具参数保持原始字符串。适配器实现在内部可以抛出异常或发出失败 finish`LlmService` 会将两者都暴露为终止失败 finish。适配器理由见[双 LLM 适配器](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md),服务边界见[终止失败决策](../../../.agents/notes/implemented/architecture/2026-07-29-terminal-llm-stream-failures.md)。
两个适配器使用不同内部机制实现 `LlmAdapter`[`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) 针对 `deepseek-official` 路由使用直接 fetch 加 `eventsource-parser` SSEServer-Sent Events分帧[`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) 则通过 `@earendil-works/pi-ai` 动态解析已配置提供方/模型对。两者都遵循 `types.ts` 中的 `StreamChunk` 约定usage 先于 finish工具参数保持原始字符串。适配器实现在内部可以抛出异常或发出失败 finish`LlmRuntime` 会将两者都暴露为终止失败 finish。适配器理由见[双 LLM 适配器](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md),服务边界见[终止失败决策](../../../.agents/notes/implemented/architecture/2026-07-29-terminal-llm-stream-failures.md)。
## 模型体验

View File

@@ -1,6 +1,6 @@
/**
* LLM service: adapter registry with a waterfall-interceptable streaming call
* API. Exports the `LlmService` default, the abstract `LlmAdapter` for
* API. Exports the `LlmRuntime` default, the abstract `LlmAdapter` for
* provider backends, and `BlockAssembler` for chunk assembly.
*
* @module @deepseek-ai/dsh-llm
@@ -45,13 +45,13 @@ export type { LlmCallConfig, LlmCallConfigAdapterDefaults } from './call-config.
declare module '@deepseek-ai/cordis' {
interface Context {
llm: LlmService
llm: LlmRuntime
}
interface Events {
/**
* Waterfall around every streaming model call (retry, replay, routing).
* Bound to the {@link LlmService}; call `next()` to reach the resolved
* Bound to the {@link LlmRuntime}; call `next()` to reach the resolved
* adapter's stream, or yield your own chunks to short-circuit.
* @param options - the full request. A LOOP-built request carries the
* process-local {@link markAgentLoopRequest} identity and arrives deep-frozen
@@ -61,7 +61,7 @@ declare module '@deepseek-ai/cordis' {
* the immutable creation contract.
* @mode waterfall
*/
'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
'llm/stream'(this: LlmRuntime, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
}
}
@@ -233,7 +233,7 @@ export abstract class LlmAdapter {
}
/**
* What {@link LlmService.registerAdapter} returns: the disposer, plus an
* What {@link LlmRuntime.registerAdapter} returns: the disposer, plus an
* atomic route replacement for the same adapter instance.
*/
export interface AdapterRegistrationHandle {
@@ -281,7 +281,7 @@ export interface DirectoryRegistrationHandle {
* The abstract `llm` service: an adapter registry plus a streaming model-call
* API, interceptable via the `llm/stream` waterfall.
*/
export class LlmService extends Service {
export class LlmRuntime extends Service {
private adapters = new Map<string, AdapterRegistration>()
private directory = new Map<string, LlmConfigurableProvider>()
private discoveries = new Map<
@@ -342,7 +342,7 @@ export class LlmService extends Service {
// The disposer has run: `owned` being empty cannot say so on its own,
// because `replace([])` legally leaves a live registration holding none.
let released = false
const dispose = this.ctx.effect(function* (this: LlmService) {
const dispose = this.ctx.effect(function* (this: LlmRuntime) {
if (providers.length === 0) throw new LlmError('an adapter must register at least one provider', 'INVALID_ADAPTER')
this.commitRoutes(owned, this.prepareRoutes(providers, adapter, owned))
yield () => {
@@ -460,7 +460,7 @@ export class LlmService extends Service {
this.emitAdaptersUpdated()
}
const dispose = this.ctx.effect(function* (this: LlmService) {
const dispose = this.ctx.effect(function* (this: LlmRuntime) {
if (entries.length === 0) {
throw new LlmError('a configurable-provider registration must declare at least one provider', 'INVALID_DIRECTORY')
}
@@ -505,7 +505,7 @@ export class LlmService extends Service {
settingsNs: string,
discover: (request: LlmModelDiscoveryRequest) => Promise<readonly LlmDiscoveredModel[]>,
): () => void {
const dispose = this.ctx.effect(function* (this: LlmService) {
const dispose = this.ctx.effect(function* (this: LlmRuntime) {
if (settingsNs.length === 0) {
throw new LlmError('model discovery needs a non-empty settings namespace', 'INVALID_DISCOVERY')
}
@@ -944,4 +944,4 @@ interface AdapterRegistration {
readonly retryPolicy: ResolvedRetryPolicy
}
export default LlmService
export default LlmRuntime

View File

@@ -12,7 +12,7 @@ export interface AssistantProvenance {
model: string
/**
* Lossless-JSON adapter state needed to replay the provider response.
* `LlmService` exposes it to a target adapter only when that adapter instance
* `LlmRuntime` exposes it to a target adapter only when that adapter instance
* currently owns both this historical provider and the target provider.
*/
replayState?: unknown

View File

@@ -285,7 +285,7 @@ export interface LlmResolvedModelInfo extends LlmModelInfo {
* Block indexes correlate interleaved deltas, and `block-end` carries the
* assembled block. Adapters emit usage before the terminal finish and nothing
* afterward; tool arguments remain raw JSON strings. An adapter implementation
* may throw, but `LlmService.stream()` normalizes that failure to a terminal
* may throw, but `LlmRuntime.stream()` normalizes that failure to a terminal
* `error` or `aborted` finish before exposing it to consumers.
*/
export type StreamChunk =

View File

@@ -1,13 +1,13 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import * as LlmInvariant from '@deepseek-ai/dsh-llm/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(LlmInvariant)
return ctx
}
@@ -95,7 +95,7 @@ describe('adapters-updated invariants', () => {
it('accepts a coherent registry at every topology notification', async () => {
const ctx = await setup()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const dispose = ctx.llm.registerAdapter(['coherent'], new NoopAdapter())
ctx.llm.registerConfigurableProviders([
{ provider: 'dormant', displayName: 'Dormant', settingsNs: 'ns', settingsPath: [] },
@@ -110,7 +110,7 @@ describe('adapters-updated invariants', () => {
})
it('reports a notification whose registry cannot be re-read', async () => {
class BrokenLlm extends LlmService {
class BrokenLlm extends LlmRuntime {
override providerRetryPolicy(_provider: string): never {
throw new Error('registration vanished')
}

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService, {
import LlmRuntime, {
errorChain,
GenerateOptions,
HarnessError,
@@ -98,7 +98,7 @@ async function collect(stream: AsyncIterable<StreamChunk>): Promise<StreamChunk[
return chunks
}
describe('LlmService', () => {
describe('LlmRuntime', () => {
it('recognizes structured and model-capacity context-window overflow details', () => {
expect(isContextWindowExceededError('context_length_exceeded maximum context length')).toBe(true)
expect(isContextWindowExceededError('context-window-overflowed')).toBe(true)
@@ -175,7 +175,7 @@ describe('LlmService', () => {
it('routes stream() to the registered adapter', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['test-provider'], new ScriptedAdapter(SCRIPT))
const chunks: StreamChunk[] = []
@@ -185,7 +185,7 @@ describe('LlmService', () => {
it('trusts the immutable message creation boundary for direct calls', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const adapter = new RecordingAdapter(SCRIPT)
ctx.llm.registerAdapter(['test-provider'], adapter)
const message = createMessage({
@@ -211,7 +211,7 @@ describe('LlmService', () => {
}
}(SCRIPT)
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['configured', 'defaulted'], adapter)
expect(ctx.llm.providerRetryPolicy('configured')).toBe(configured)
@@ -229,7 +229,7 @@ describe('LlmService', () => {
const newPolicy = resolveRetryPolicy({ mode: 'normal', maxRetries: 0 }, 'new retryPolicy')
const oldFailure = new LlmError('old route failed', 'AUTH')
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const disposeOld = ctx.llm.registerAdapter(['route'], new class extends ThrowingAdapter {
override providerRetryPolicy(): typeof oldPolicy {
return oldPolicy
@@ -258,7 +258,7 @@ describe('LlmService', () => {
it('normalizes an unregistered provider to a terminal failure', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const chunks = await collect(ctx.llm.stream({
provider: 'nope',
@@ -298,7 +298,7 @@ describe('LlmService', () => {
}
}()
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['test'], adapter)
const chunks = await collect(ctx.llm.stream({
@@ -326,7 +326,7 @@ describe('LlmService', () => {
}
}()
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['test'], adapter)
const chunks = await collect(ctx.llm.stream({
@@ -351,7 +351,7 @@ describe('LlmService', () => {
requestId: ProviderRequestId('req-7'),
})
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['test'], new ThrowingAdapter(failure))
const chunks = await collect(ctx.llm.stream({
@@ -390,7 +390,7 @@ describe('LlmService', () => {
}
}()
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['test'], adapter)
const chunks = await collect(ctx.llm.stream({
@@ -412,7 +412,7 @@ describe('LlmService', () => {
const controller = new AbortController()
controller.abort('cancelled')
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['test'], new ThrowingAdapter(new Error('stopped')))
const chunks = await collect(ctx.llm.stream({
@@ -431,7 +431,7 @@ describe('LlmService', () => {
it('leaves middleware and consumer failures thrown', async () => {
const middlewareFailure = new Error('middleware failed')
const middlewareCtx = new Context()
await middlewareCtx.plugin(LlmService)
await middlewareCtx.plugin(LlmRuntime)
middlewareCtx.llm.registerAdapter(['test'], new ScriptedAdapter(SCRIPT))
middlewareCtx.on('llm/stream', () => (async function* () {
throw middlewareFailure
@@ -444,7 +444,7 @@ describe('LlmService', () => {
const consumerFailure = new Error('consumer failed')
const consumerCtx = new Context()
await consumerCtx.plugin(LlmService)
await consumerCtx.plugin(LlmRuntime)
consumerCtx.llm.registerAdapter(['test'], new ScriptedAdapter(SCRIPT))
await expect((async () => {
for await (const _chunk of consumerCtx.llm.stream({
@@ -476,7 +476,7 @@ describe('LlmService', () => {
}
}()
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['test'], adapter)
await expect((async () => {
@@ -500,7 +500,7 @@ describe('LlmService', () => {
}
}()
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['test'], adapter)
for await (const _chunk of ctx.llm.stream({ provider: 'test', model: 'test', messages: [] })) break
@@ -508,7 +508,7 @@ describe('LlmService', () => {
it('unregisters adapters when the owning fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
inner.llm.registerAdapter(['scoped-model'], new ScriptedAdapter(SCRIPT))
@@ -521,7 +521,7 @@ describe('LlmService', () => {
it('discovers detached provider and advisory model metadata', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const provider = { id: 'catalog', name: 'Catalog Provider' }
const model = { provider: 'catalog', id: 'fast', name: 'Fast', description: 'Low latency' }
ctx.llm.registerAdapter(['catalog'], new CatalogAdapter(provider, [model]))
@@ -543,7 +543,7 @@ describe('LlmService', () => {
it('defaults adapters to their route name and an empty advisory model list', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['plain'], new ScriptedAdapter(SCRIPT))
expect(ctx.llm.listProviders()).toEqual([{ id: 'plain', name: 'plain' }])
await expect(ctx.llm.listModels('plain')).resolves.toEqual([])
@@ -564,7 +564,7 @@ describe('LlmService', () => {
[{ provider: 'route', id: 'model', name: 'Model', description: 1 }, 'non-string description'],
] as const)('rejects invalid exact model metadata (%s: %s)', async (metadata, _label) => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const adapter = new class extends ScriptedAdapter {
override resolveModel(): Promise<LlmResolvedModelInfo> {
return Promise.resolve(metadata as unknown as LlmResolvedModelInfo)
@@ -578,7 +578,7 @@ describe('LlmService', () => {
it('preserves modality metadata through exact model resolution', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const adapter = new class extends ScriptedAdapter {
override resolveModel(): Promise<LlmResolvedModelInfo> {
return Promise.resolve({
@@ -599,7 +599,7 @@ describe('LlmService', () => {
it('resolves detached model context independently of advisory catalog membership', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const source = { contextWindow: 32_000 }
ctx.llm.registerAdapter(['route'], new CatalogAdapter(
{ id: 'route', name: 'Route' },
@@ -618,7 +618,7 @@ describe('LlmService', () => {
it('resolves detached adapter-owned reasoning metadata and materializes its default', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const source = {
efforts: [
{ id: ReasoningEffortId('standard'), name: 'Standard' },
@@ -655,7 +655,7 @@ describe('LlmService', () => {
it('materializes an adapter-owned maxTokens default while preserving an explicit cap', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['route'], new CatalogAdapter(
{ id: 'route', name: 'Route' },
[],
@@ -684,7 +684,7 @@ describe('LlmService', () => {
'rejects invalid adapter-owned default maxTokens %s',
async (defaultMaxTokens) => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const adapter = new class extends ScriptedAdapter {
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
return Promise.resolve({ provider, id: model, name: model, defaultMaxTokens })
@@ -706,7 +706,7 @@ describe('LlmService', () => {
[{ efforts: [{ id: 'valid', name: 'Valid' }], defaultEffort: 'other' }, 'unknown default'],
] as const)('rejects invalid model reasoning metadata (%s: %s)', async (metadata, _label) => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['route'], new CatalogAdapter(
{ id: 'route', name: 'Route' },
[],
@@ -719,7 +719,7 @@ describe('LlmService', () => {
it('rejects unsupported reasoning efforts without clamping', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['route'], new CatalogAdapter(
{ id: 'route', name: 'Route' },
[],
@@ -741,7 +741,7 @@ describe('LlmService', () => {
it('resolves reasoning defaults at the final adapter boundary after routing middleware', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const adapter = new class extends RecordingAdapter {
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
const reasoning: LlmModelReasoningInfo = {
@@ -783,7 +783,7 @@ describe('LlmService', () => {
it('pins one adapter registration across asynchronous exact-model resolution and dispatch', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const started = Promise.withResolvers<undefined>()
const reasoning = Promise.withResolvers<LlmModelReasoningInfo>()
const first = new class extends RecordingAdapter {
@@ -826,7 +826,7 @@ describe('LlmService', () => {
it('prepares a one-shot registration-bound call and rejects config drift', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const adapter = new CatalogAdapter(
{ id: 'route', name: 'Route' },
[],
@@ -875,7 +875,7 @@ describe('LlmService', () => {
it('reuses one exact-model lookup for prepared config and context metadata', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
let resolutions = 0
const source = { contextWindow: 128_000 }
const adapter = new class extends ScriptedAdapter {
@@ -917,7 +917,7 @@ describe('LlmService', () => {
it('passes cancellation through exact-model resolution', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const started = Promise.withResolvers<undefined>()
const adapter = new class extends ScriptedAdapter {
override resolveModel(
@@ -958,7 +958,7 @@ describe('LlmService', () => {
'rejects invalid adapter model context %s',
async (contextWindow) => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['route'], new CatalogAdapter(
{ id: 'route', name: 'Route' },
[],
@@ -976,7 +976,7 @@ describe('LlmService', () => {
[{ id: 'route', name: '' }, 'empty name'],
] as const)('rejects invalid provider metadata atomically (%s: %s)', async (metadata, _label) => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const adapter = new CatalogAdapter(metadata as unknown as LlmProviderInfo, [])
expect(() => ctx.llm.registerAdapter(['route'], adapter)).toThrow(expect.objectContaining({ code: 'INVALID_ADAPTER' }))
expect(ctx.llm.listProviders()).toEqual([])
@@ -992,7 +992,7 @@ describe('LlmService', () => {
[{ provider: 'route', id: 'm', name: 'M', description: 1 }, 'non-string description'],
] as const)('rejects invalid model metadata (%s: %s)', async (metadata, _label) => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['route'], new CatalogAdapter(
{ id: 'route', name: 'Route' },
[metadata as unknown as LlmModelInfo],
@@ -1002,7 +1002,7 @@ describe('LlmService', () => {
it('rejects duplicate model ids in one provider catalog', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const model = { provider: 'route', id: 'same', name: 'Same' }
ctx.llm.registerAdapter(['route'], new CatalogAdapter({ id: 'route', name: 'Route' }, [model, model]))
await expect(ctx.llm.listModels('route')).rejects.toMatchObject({ code: 'INVALID_CATALOG' })
@@ -1010,7 +1010,7 @@ describe('LlmService', () => {
it('lets llm/stream waterfall listeners wrap the underlying stream', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['test-model'], new ScriptedAdapter(SCRIPT))
ctx.on('llm/stream', function (_options, next) {
@@ -1030,7 +1030,7 @@ describe('LlmService', () => {
it('resolves the provider after llm/stream listeners have had a chance to route it', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const adapter = new RecordingAdapter(SCRIPT)
ctx.llm.registerAdapter(['routed'], adapter)
ctx.on('llm/stream', (options, next) => {
@@ -1044,7 +1044,7 @@ describe('LlmService', () => {
it('keeps replay state when historical and target providers belong to the same adapter instance', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const adapter = new RecordingAdapter(SCRIPT)
ctx.llm.registerAdapter(['historical', 'target'], adapter)
const replayState = { private: 'state' }
@@ -1069,7 +1069,7 @@ describe('LlmService', () => {
it('strips replay state but preserves provider and model when the target uses a different adapter instance', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['historical'], new RecordingAdapter(SCRIPT))
const target = new RecordingAdapter(SCRIPT)
ctx.llm.registerAdapter(['target'], target)
@@ -1096,7 +1096,7 @@ describe('LlmService', () => {
it('preserves immutability while stripping replay state from frozen requests', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['historical'], new RecordingAdapter(SCRIPT))
const target = new RecordingAdapter(SCRIPT)
ctx.llm.registerAdapter(['target'], target)
@@ -1169,7 +1169,7 @@ describe('LlmService', () => {
it('removes the adapter when the returned disposer is called', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const dispose = ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
expect(ctx.llm.listProviders()).toEqual([{ id: 'm1', name: 'm1' }])
@@ -1179,7 +1179,7 @@ describe('LlmService', () => {
it('rejects duplicate adapter registration with DUPLICATE_ADAPTER code', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
try {
ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
@@ -1193,7 +1193,7 @@ describe('LlmService', () => {
it('rejects empty and internally duplicated provider registrations atomically', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const adapter = new ScriptedAdapter(SCRIPT)
expect(() => ctx.llm.registerAdapter([], adapter)).toThrow(expect.objectContaining({ code: 'INVALID_ADAPTER' }))
@@ -1204,7 +1204,7 @@ describe('LlmService', () => {
it('re-registers a model after its prior registration is disposed', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const dispose = ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
expect(ctx.llm.listProviders()).toEqual([{ id: 'm1', name: 'm1' }])
@@ -1222,7 +1222,7 @@ describe('LlmService', () => {
// The leak this prevents: the effect's disposer has run, so a route added
// afterwards would sit in the registry with nothing left to release it.
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const handle = ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
handle()
@@ -1235,7 +1235,7 @@ describe('LlmService', () => {
// `replace([])` is the settings-section-emptied case: legal, and it must
// not be mistaken for disposal by the guard above.
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const handle = ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
handle.replace([])

View File

@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmConfigurableProvider, StreamChunk } from '@deepseek-ai/dsh-llm'
class NoopAdapter extends LlmAdapter {
@@ -12,7 +12,7 @@ class NoopAdapter extends LlmAdapter {
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
return ctx
}

View File

@@ -21,7 +21,7 @@
"path": "../../attachment/attachment"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../util/timeout"

View File

@@ -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/token-meter/README.md
README.md: 6cbbefa69fd4dd108d4c4ff717647e173922d7b2
README.zh.md: 4f95b4fed4c6cd2b08fbf8b02f73cc5291b52a5d
README.md: a2deab11a31285ba598b8864d3a734ecf7c56620
README.zh.md: e0c3a8aa123ca0acc374277cd92b415e0b0b3cb5

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
Replay-aware token measurement through the singleton `ctx.tokenMeter` service. It advances one isolated fold per session from the durable log, so compaction and other pressure-sensitive plugins can share accounting without depending on `CompactService`.
Replay-aware token measurement through the singleton `ctx.tokenMeter` service. It advances one isolated fold per session from the durable log, so compaction and other pressure-sensitive plugins can share accounting without depending on `CompactionEngine`.
## Configuration
@@ -47,14 +47,14 @@ The [Agent Note](../../../.agents/notes/implemented/architecture/2026-07-29-proj
```yaml
- name: '@deepseek-ai/dsh-token-meter'
- name: '@deepseek-ai/dsh-compact-basic'
- name: '@deepseek-ai/dsh-compaction-basic'
```
Both plugins have usable defaults. The meter remains independent of model routing and optional compaction. A deployment configures capacity on its LLM adapter and compaction policy on `dsh-compact-basic`.
Both plugins have usable defaults. The meter remains independent of model routing and optional compaction. A deployment configures capacity on its LLM adapter and compaction policy on `dsh-compaction-basic`.
## Model Experience
Indirectly, through consumers such as `dsh-compact-basic`; the service itself adds no prompt, message, schema, tool, or model call.
Indirectly, through consumers such as `dsh-compaction-basic`; the service itself adds no prompt, message, schema, tool, or model call.
#### KV Cache effect

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
通过单例 `ctx.tokenMeter` 服务进行具备回放感知能力的 token 测量。它从持久日志为每个会话推进一个隔离 fold因此压缩compaction与其他压力敏感插件可以共享计量无需依赖 `CompactService`
通过单例 `ctx.tokenMeter` 服务进行具备回放感知能力的 token 测量。它从持久日志为每个会话推进一个隔离 fold因此压缩compaction与其他压力敏感插件可以共享计量无需依赖 `CompactionEngine`
## 配置
@@ -47,14 +47,14 @@ fold 跟踪完整请求标头快照、步骤边界、表层追加与替换、成
```yaml
- name: '@deepseek-ai/dsh-token-meter'
- name: '@deepseek-ai/dsh-compact-basic'
- name: '@deepseek-ai/dsh-compaction-basic'
```
两个插件都有可用默认值。meter 保持与模型路由和可选压缩无关。部署会在 LLM大语言模型适配器上配置容量并在 `dsh-compact-basic` 上配置压缩策略。
两个插件都有可用默认值。meter 保持与模型路由和可选压缩无关。部署会在 LLM大语言模型适配器上配置容量并在 `dsh-compaction-basic` 上配置压缩策略。
## 模型体验
通过 `dsh-compact-basic` 等消费方间接影响该服务自身不添加提示词、消息、schema、工具或模型调用。
通过 `dsh-compaction-basic` 等消费方间接影响该服务自身不添加提示词、消息、schema、工具或模型调用。
#### KV Cache 影响

View File

@@ -37,7 +37,7 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-compact": "workspace:^",
"@deepseek-ai/dsh-compaction": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
@@ -49,7 +49,7 @@
"zod": "^4.4.3"
},
"devDependencies": {
"@deepseek-ai/dsh-compact": "workspace:^",
"@deepseek-ai/dsh-compaction": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",

View File

@@ -66,12 +66,12 @@ function validateConfigKeys(config: TokenMeterConfig): void {
declare module '@deepseek-ai/cordis' {
interface Context {
tokenMeter: TokenMeterService
tokenMeter: TokenMeter
}
}
/** Replay owner for one service-wide estimator and isolated per-session folds. */
export class TokenMeterService extends Service {
export class TokenMeter extends Service {
// Schemastery preserves untrusted loader keys on an empty object schema;
// the public type excludes settings while validateConfigKeys rejects them.
static Config: z<TokenMeterConfig> = z.object({}) as unknown as z<TokenMeterConfig>
@@ -310,4 +310,4 @@ export class TokenMeterService extends Service {
}
}
export default TokenMeterService
export default TokenMeter

View File

@@ -6,7 +6,7 @@
* (one node per model-visible message) would grow a checkpoint without
* bound over the session's life. Instead, replacements ride the compact
* seam's shadow-price protocol: the metering event immediately before a
* surface `replace` (`compact/summary` or `compact/prune`) states the
* surface `replace` (`compaction/summary` or `compaction/prune`) states the
* heuristic price of the exact replaced range, so the fold keeps a running
* total plus at most one pending claim and never retains per-node prices.
* The counts are exact by construction: producers derive them from the same
@@ -19,8 +19,8 @@
import { deriveEventMessage, isSurfaceEvent } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
// Type-only: the `compact/*` SessionEventMap merges (shadow-price events).
import type {} from '@deepseek-ai/dsh-compact'
// Type-only: the `compaction/*` SessionEventMap merges (shadow-price events).
import type {} from '@deepseek-ai/dsh-compaction'
import { estimateMessage } from './estimate.ts'
/**
@@ -67,7 +67,7 @@ export function foldSurfaceProjection(
claim: ShadowPriceClaim | undefined,
event: SessionEvent,
): SurfaceTokensFold {
if (event.type === 'compact/summary' || event.type === 'compact/prune') {
if (event.type === 'compaction/summary' || event.type === 'compaction/prune') {
const { shadowedRange, shadowedTokenCount } = event.data
return {
deltaTokens: 0,

View File

@@ -8,9 +8,9 @@ import type { ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import type { ContextBreakdownProjection } from '@deepseek-ai/dsh-token-meter/client'
import { CompactionId } from '@deepseek-ai/dsh-compact'
import { CompactionId } from '@deepseek-ai/dsh-compaction'
import { contextBreakdownProjectionDefinition } from '../src/breakdown-projection.ts'
import {
estimateContent,
@@ -32,7 +32,7 @@ async function harness(): Promise<{ ctx: Context; session: Session }> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(TokenMeterService)
await ctx.plugin(TokenMeter)
return { ctx, session: ctx.sessions.create() }
}
@@ -50,7 +50,7 @@ function appendUser(session: Session, text: string): number {
}
/**
* Meter one upcoming replacement the way compact-basic does: price the
* Meter one upcoming replacement the way compaction-basic does: price the
* replaced span from the measurement service's own nodes and log the
* shadow-price event directly before the replace.
*/
@@ -59,7 +59,7 @@ function appendSummaryMeter(ctx: Context, session: Session, start: number, end:
const startIdx = nodes.findIndex(node => node.seq === start)
const endIdx = nodes.findIndex(node => node.seq === end)
const shadowed = nodes.slice(startIdx, endIdx + 1)
session.append('compact/summary', {
session.append('compaction/summary', {
compactionId: CompactionId('context-breakdown-summary'),
summary: [{ type: 'text', text: 'summary' }],
shadowedRange: { start, end },
@@ -200,7 +200,7 @@ describe('contextBreakdown session projection', () => {
surfaceOp: 'append',
} as unknown as SessionEvent)
const meter = (start: number, end: number, seq: number): SessionEvent => ({
type: 'compact/prune',
type: 'compaction/prune',
seq,
time: 0,
data: { shadowedRange: { start, end }, shadowedSeqs: [start, end], shadowedTokenCount: 5 },
@@ -260,7 +260,7 @@ describe('contextBreakdown session projection', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
const meterFiber = await ctx.plugin(TokenMeterService)
const meterFiber = await ctx.plugin(TokenMeter)
const session = ctx.sessions.create()
session.append('request/header', {
header: { config: CONFIG, system: 'You are terse.' },
@@ -274,7 +274,7 @@ describe('contextBreakdown session projection', () => {
await meterFiber.dispose()
expect(ctx.sessionProjections.snapshot(session).values).not.toHaveProperty('contextBreakdown')
await ctx.plugin(TokenMeterService)
await ctx.plugin(TokenMeter)
expect(ctx.sessionProjections.viewCheckpoint(checkpoint).contextBreakdown).toEqual({
systemTokens: estimateSystemTokens({ config: CONFIG, system: 'You are terse.' }),
toolsTokens: 0,

View File

@@ -4,7 +4,7 @@ import { createUserMessage, CallId, createMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, Message, TokenUsage } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId, canonicalHeader } from '@deepseek-ai/dsh-session'
import type { EpochHeader, SessionEvent } from '@deepseek-ai/dsh-session'
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import type { TokenMeasurement, TokenMeterConfig } from '@deepseek-ai/dsh-token-meter'
function header(model: string, extras: Omit<EpochHeader, 'config'> = {}): EpochHeader {
@@ -89,8 +89,8 @@ function appendSuccessfulCall(
session.append('step/end', { turn, step })
}
function meter(config: TokenMeterConfig = {}): TokenMeterService {
return new TokenMeterService(new Context(), config)
function meter(config: TokenMeterConfig = {}): TokenMeter {
return new TokenMeter(new Context(), config)
}
function expectSurfaceTotal(measurement: TokenMeasurement): void {
@@ -98,7 +98,7 @@ function expectSurfaceTotal(measurement: TokenMeasurement): void {
.toBe(measurement.surfaceTokens)
}
describe('TokenMeterService configuration and registration', () => {
describe('TokenMeter configuration and registration', () => {
it('exposes an empty public configuration type', () => {
expectTypeOf<{}>().toExtend<TokenMeterConfig>()
expectTypeOf<{ contextWindow: number }>().not.toExtend<TokenMeterConfig>()
@@ -115,14 +115,14 @@ describe('TokenMeterService configuration and registration', () => {
it('registers and unregisters ctx.tokenMeter with its plugin fiber', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const fiber = await ctx.plugin(TokenMeterService)
expect(ctx.get('tokenMeter')).toBeInstanceOf(TokenMeterService)
const fiber = await ctx.plugin(TokenMeter)
expect(ctx.get('tokenMeter')).toBeInstanceOf(TokenMeter)
await fiber.dispose()
expect(ctx.get('tokenMeter')).toBeUndefined()
})
})
describe('TokenMeterService pricing', () => {
describe('TokenMeter pricing', () => {
it('prices every built-in content shape and merge-extended blocks with one fixed heuristic', () => {
const service = meter()
const blocks: ContentBlock[] = [
@@ -438,7 +438,7 @@ describe('replay anchors and surface folds', () => {
})
describe('malformed replay and listener lifecycle', () => {
function expectRepeatedFailure(service: TokenMeterService, session: Session, pattern: RegExp): void {
function expectRepeatedFailure(service: TokenMeter, session: Session, pattern: RegExp): void {
expect(() => service.measure(session)).toThrow(pattern)
expect(() => service.measure(session)).toThrow(pattern)
}
@@ -660,12 +660,12 @@ describe('malformed replay and listener lifecycle', () => {
it('handles earlier-reader catch-up, eager observation, and service reload', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
let activeMeter: TokenMeterService | undefined
let activeMeter: TokenMeter | undefined
const revisions: number[] = []
ctx.on('session/event', (session) => {
if (activeMeter !== undefined) revisions.push(activeMeter.measure(session).logRevision)
})
const firstFiber = await ctx.plugin(TokenMeterService)
const firstFiber = await ctx.plugin(TokenMeter)
activeMeter = ctx.tokenMeter
const session = ctx.sessions.create(SessionId('listener-order'), { seed: [{
type: 'turn/start',
@@ -684,7 +684,7 @@ describe('malformed replay and listener lifecycle', () => {
expect(activeMeter.measure(session).logRevision).toBe(3)
await firstFiber.dispose()
const secondFiber = await ctx.plugin(TokenMeterService)
const secondFiber = await ctx.plugin(TokenMeter)
activeMeter = ctx.tokenMeter
expect(activeMeter.measure(session).logRevision).toBe(3)
await secondFiber.dispose()

View File

@@ -5,9 +5,9 @@ import type { TokenUsage } from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import type { Session } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import type { ContextPressureProjection, TokenUsageProjection } from '@deepseek-ai/dsh-token-meter/client'
import { CompactionId } from '@deepseek-ai/dsh-compact'
import { CompactionId } from '@deepseek-ai/dsh-compaction'
const ZERO: TokenUsageProjection = {
uncachedInputTokens: 0,
@@ -24,7 +24,7 @@ async function harness(): Promise<{
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
const meterFiber = await ctx.plugin(TokenMeterService)
const meterFiber = await ctx.plugin(TokenMeter)
return { ctx, session: ctx.sessions.create(), meterFiber }
}
@@ -72,7 +72,7 @@ const projected = (ctx: Context, session: Session): TokenUsageProjection => {
}
/**
* Meter one upcoming replacement the way compact-basic does: price the
* Meter one upcoming replacement the way compaction-basic does: price the
* replaced span from the measurement service's own nodes and log the
* shadow-price event directly before the replace.
*/
@@ -81,7 +81,7 @@ function appendSummaryMeter(ctx: Context, session: Session, start: number, end:
const startIdx = nodes.findIndex(node => node.seq === start)
const endIdx = nodes.findIndex(node => node.seq === end)
const shadowed = nodes.slice(startIdx, endIdx + 1)
session.append('compact/summary', {
session.append('compaction/summary', {
compactionId: CompactionId('token-usage-summary'),
summary: [{ type: 'text', text: 'summary' }],
shadowedRange: { start, end },
@@ -234,7 +234,7 @@ describe('tokenUsage session projection', () => {
await meterFiber.dispose()
expect(ctx.sessionProjections.snapshot(session).values).not.toHaveProperty('tokenUsage')
await ctx.plugin(TokenMeterService)
await ctx.plugin(TokenMeter)
expect(ctx.sessionProjections.viewCheckpoint(checkpoint).tokenUsage).toEqual({
uncachedInputTokens: 8,
outputTokens: 2,
@@ -379,7 +379,7 @@ describe('contextPressure session projection', () => {
await meterFiber.dispose()
expect(ctx.sessionProjections.snapshot(session).values).not.toHaveProperty('contextPressure')
await ctx.plugin(TokenMeterService)
await ctx.plugin(TokenMeter)
expect(ctx.sessionProjections.viewCheckpoint(checkpoint).contextPressure).toEqual({
pressureTokens: 42,
projectedTokens: 42,

View File

@@ -24,13 +24,13 @@
"path": "../../core/session"
},
{
"path": "../../compact/compact"
"path": "../../compaction/compaction"
},
{
"path": "../../session/session-projection"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}