fix: address ds-review-bot v8 findings

- llm-deepseek: the uncatalogued resolveModel fallback declares text-only
  modalities — the wire route is text-only regardless of catalog
  membership, so "unknown" must not let the host persist-then-fail images.
- session.selectModel also consults the pending-inbox mirror: a queued
  image prompt enters the log only when claimed, after a switch would land.
- attachment store: ensureDurableDirectory syncs every ancestor entry up to
  a caller-vouched boundary regardless of what mkdir reports — a raced
  "already existed" is not "already durable".
- One image walker (imageBlockIn/imageInEvent) now serves both attachment
  authorization and the selection gate; referencedImage therefore also
  authorizes references inside wrapped message content.
- InputHub: the scope disposer resolves the conversation service optionally
  (teardown/HMR must reach quiescence), and a send failing after its scope
  died releases the in-flight drafts instead of restoring them onto a
  disposed shell.
- http-bridge destroys declared-oversize requests with connection: close
  instead of draining a body the client can trickle indefinitely.
- LlmService validates AND detaches modality arrays identically on the
  advisory and exact routes; READMEs record the fourth INVALID_MODEL_INFO
  rejection reason.
- CLI provider docs (JSDoc, README pair, Agent Note pair) describe the
  reuse behavior; llm-route.spec now parses the SHIPPED cordis.yml through
  the production extraction, pinning the row coupling.
- image-display lane pins gallery/rail shape in inline snapshots and the
  object-URL scheme this environment must take; stale host.schema comment
  dropped.
This commit is contained in:
creatixchu
2026-07-29 19:40:17 +08:00
parent adce3b833d
commit f73bf425ec
27 changed files with 337 additions and 100 deletions

View File

@@ -166,8 +166,12 @@ export class DeepSeekAdapter extends LlmAdapter {
const contextWindow = configured?.contextWindow
?? this.options.defaultContextWindow
return Promise.resolve({
// The chat-completions wire route is text-only regardless of catalog
// membership, so the uncatalogued fallback declares the same negative
// capability — "unknown" here would let the host accept and persist
// images the serializer must then reject.
...configured === undefined
? { provider, id: model, name: model }
? { provider, id: model, name: model, inputModalities: ['text' as const], outputModalities: ['text' as const] }
: modelInfo(provider, configured),
...contextWindow === undefined ? {} : { context: { contextWindow } },
...this.options.defaults?.thinking === 'disabled'

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: d343449d1530bf70a3a8c57f883894e29c42d18f
README.zh.md: 6ac57b1e6010b58c45b516f13ec6361d47ca8d12
README.md: ecd6da304a894a687153608f5b468f867ad32e6b
README.zh.md: 2812f2d74ec3dbab521f8e3148e9efe2640c6929

View File

@@ -23,7 +23,7 @@ An adapter registry plus a single streaming call surface, interceptable via a wa
Provider and model metadata is a discovery surface, not a routing whitelist. `registerAdapter()` still owns provider exclusivity and captures the adapter's retry policy for each route, while an adapter may accept model ids absent from `listModels()`; consumers must not reject a request because its model is unlisted. Returned selector metadata is detached and invalid or duplicate adapter entries fail with `INVALID_ADAPTER` or `INVALID_CATALOG`.
Exact-model metadata is a separate correctness query, not a catalog decoration or global LLM setting. `resolveModelInfo()` asks the adapter that owns the exact provider/model route once; an adapter can describe an unlisted dynamic model, and absent `context` or `reasoning` fields mean only that those capabilities are unavailable. Invalid identity, context, or reasoning metadata fails with `INVALID_MODEL_INFO`, `INVALID_MODEL_CONTEXT`, or `INVALID_MODEL_REASONING`.
Exact-model metadata is a separate correctness query, not a catalog decoration or global LLM setting. `resolveModelInfo()` asks the adapter that owns the exact provider/model route once; an adapter can describe an unlisted dynamic model, and absent `context` or `reasoning` fields mean only that those capabilities are unavailable. Invalid identity or modality metadata fails with `INVALID_MODEL_INFO`, and invalid context or reasoning metadata with `INVALID_MODEL_CONTEXT` or `INVALID_MODEL_REASONING`.
Reasoning identifiers are opaque adapter-owned strings rather than a core enum. An adapter publishes its ordered selectable list, including an `off` id when that model's capability API exposes one. `resolveCallConfig()` accepts only an exact advertised identifier, materializes `defaultEffort` when present, and otherwise preserves the provider default. Asynchronous model resolvers receive the caller's signal and must settle promptly after cancellation. `prepareCall()` additionally retains the exact adapter registration through header logging and terminal dispatch, so HMR cannot combine one adapter's capability result with another adapter's request; reusing its one-shot handle or changing its call-config fields fails with `INVALID_PREPARED_CALL`. An unsupported explicit or configured effort fails with `UNSUPPORTED_REASONING_EFFORT` before provider I/O.

View File

@@ -23,7 +23,7 @@
提供方与模型元数据是发现表层,不是路由白名单。`registerAdapter()` 仍拥有提供方排他性,并为每条路由捕获适配器的重试策略;适配器则可以接受 `listModels()` 中不存在的模型 id消费方禁止因模型未列出而拒绝请求。返回的 selector 元数据与输入脱离,无效或重复适配器配置项会以 `INVALID_ADAPTER``INVALID_CATALOG` 失败。
确切模型元数据是独立的正确性查询,不是 catalog 装饰或全局 LLM 设置。`resolveModelInfo()` 会向拥有精确提供方/模型路由的适配器查询一次;适配器可以描述未列出的动态模型,缺少 `context``reasoning` 字段只表示相应能力不可用。无效的身份、上下文或推理元数据会以 `INVALID_MODEL_INFO``INVALID_MODEL_CONTEXT``INVALID_MODEL_REASONING` 失败。
确切模型元数据是独立的正确性查询,不是 catalog 装饰或全局 LLM 设置。`resolveModelInfo()` 会向拥有精确提供方/模型路由的适配器查询一次;适配器可以描述未列出的动态模型,缺少 `context``reasoning` 字段只表示相应能力不可用。无效的身份或模态元数据会以 `INVALID_MODEL_INFO` 失败,无效的上下文或推理元数据则以 `INVALID_MODEL_CONTEXT``INVALID_MODEL_REASONING` 失败。
推理标识符是由适配器持有的不透明字符串,而非核心枚举。适配器会公布有序可选列表;模型能力 API 提供 `off` id 时,列表也会包含它。`resolveCallConfig()` 只接受与已公布标识符完全一致的值,在存在 `defaultEffort` 时填入它,否则保留提供方默认值。异步模型解析器会接收调用方的 signal并且必须在取消后迅速完成结算。`prepareCall()` 还会让精确适配器注册跨越请求头记录和最终分派,因此 HMR热模块替换不会将一个适配器的能力结果与另一个适配器的请求混用复用其一次性句柄或更改调用配置字段会以 `INVALID_PREPARED_CALL` 失败。不支持的显式或配置推理强度会在提供方 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败。

View File

@@ -13,6 +13,7 @@ import type {
LlmModelInfo,
LlmResolvedModelInfo,
LlmProviderInfo,
ModelModality,
StreamChunk,
} from './types.ts'
import { freezeMessage, type Message } from './message.ts'
@@ -253,6 +254,28 @@ export class LlmService extends Service {
return this.registration(provider).retryPolicy
}
/**
* Validate adapter-owned modality arrays and detach them. One rule for the
* advisory catalog and exact resolution: both validate, both copy — two
* readings of the same adapter field with different trust or detachment
* would be an unexplained asymmetry.
* @param provider - provider route (diagnostic context).
* @param code - error code matching the calling surface.
* @param modalities - adapter-owned array, or undefined for unknown.
* @returns a detached copy, or undefined when absent.
*/
private detachedModalities(
provider: string,
code: 'INVALID_CATALOG' | 'INVALID_MODEL_INFO',
modalities: readonly unknown[] | undefined,
): ModelModality[] | undefined {
if (modalities === undefined) return undefined
if (!Array.isArray(modalities) || modalities.some(entry => typeof entry !== 'string')) {
throw new LlmError(`adapter returned invalid modality metadata for provider "${provider}"`, code)
}
return [...(modalities as readonly ModelModality[])]
}
/**
* Discover models advertised by one registered provider. Catalog membership
* is advisory and never changes routing or request validation.
@@ -277,13 +300,15 @@ export class LlmService extends Service {
throw new LlmError(`adapter returned invalid or duplicate model metadata for provider "${provider}"`, 'INVALID_CATALOG')
}
seen.add(model.id)
const inputModalities = this.detachedModalities(provider, 'INVALID_CATALOG', model.inputModalities)
const outputModalities = this.detachedModalities(provider, 'INVALID_CATALOG', model.outputModalities)
return {
provider: model.provider,
id: model.id,
name: model.name,
...model.description === undefined ? {} : { description: model.description },
...model.inputModalities === undefined ? {} : { inputModalities: [...model.inputModalities] },
...model.outputModalities === undefined ? {} : { outputModalities: [...model.outputModalities] },
...inputModalities === undefined ? {} : { inputModalities },
...outputModalities === undefined ? {} : { outputModalities },
}
})
}
@@ -333,23 +358,17 @@ export class LlmService extends Service {
'INVALID_MODEL_CONTEXT',
)
}
for (const modalities of [resolved.inputModalities, resolved.outputModalities]) {
if (modalities !== undefined && (!Array.isArray(modalities) || modalities.some(m => typeof m !== 'string'))) {
throw new LlmError(
`adapter returned invalid modality metadata for provider "${provider}" model "${model}"`,
'INVALID_MODEL_INFO',
)
}
}
// Capability metadata rides through: an explicit modality omission is
// negative capability downstream preflights act on (image admission).
const inputModalities = this.detachedModalities(provider, 'INVALID_MODEL_INFO', resolved.inputModalities)
const outputModalities = this.detachedModalities(provider, 'INVALID_MODEL_INFO', resolved.outputModalities)
const info: LlmResolvedModelInfo = {
provider,
id: model,
name: resolved.name,
...resolved.description === undefined ? {} : { description: resolved.description },
// Capability metadata rides through: an explicit modality omission is
// negative capability downstream preflights act on (image admission).
...resolved.inputModalities === undefined ? {} : { inputModalities: resolved.inputModalities },
...resolved.outputModalities === undefined ? {} : { outputModalities: resolved.outputModalities },
...inputModalities === undefined ? {} : { inputModalities },
...outputModalities === undefined ? {} : { outputModalities },
...context === undefined ? {} : { context: { contextWindow: context.contextWindow } },
}
const reasoning = resolved.reasoning

View File

@@ -1177,6 +1177,8 @@ describe('LlmService', () => {
[{ provider: 'route', id: 'm', name: 1 }, 'non-string name'],
[{ provider: 'route', id: 'm', name: '' }, 'empty name'],
[{ provider: 'route', id: 'm', name: 'M', description: 1 }, 'non-string description'],
[{ provider: 'route', id: 'm', name: 'M', inputModalities: 'text' }, 'non-array input modalities'],
[{ provider: 'route', id: 'm', name: 'M', outputModalities: [1] }, 'non-string output modality'],
] as const)('rejects invalid model metadata (%s: %s)', async (metadata, _label) => {
const ctx = new Context()
await ctx.plugin(LlmService)