From a36745f6ee268ba5901a686c09cb462ecc7260e2 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Tue, 27 Oct 2026 15:01:54 +0800 Subject: [PATCH] refactor(models): share listing cache lifecycle wiring --- .../llm/llm-deepseek-account/src/index.ts | 10 ++----- .../llm/llm-deepseek-api-key/src/index.ts | 10 ++----- packages/llm/llm-deepseek/README.i18n.yaml | 4 +-- packages/llm/llm-deepseek/README.md | 2 +- packages/llm/llm-deepseek/README.zh.md | 2 +- packages/llm/llm-deepseek/src/host.ts | 26 +++++++++++++++++++ packages/llm/llm-deepseek/src/index.ts | 2 +- 7 files changed, 35 insertions(+), 21 deletions(-) diff --git a/packages/llm/llm-deepseek-account/src/index.ts b/packages/llm/llm-deepseek-account/src/index.ts index 68d3b59cc8..633b4efb57 100644 --- a/packages/llm/llm-deepseek-account/src/index.ts +++ b/packages/llm/llm-deepseek-account/src/index.ts @@ -5,7 +5,7 @@ import { ACCOUNT_QUOTA_EXCEEDED_CODE, APP_IDENTITY, LlmError, QUOTA_EXCEEDED_COD import type {} from '@deepseek-ai/dsh-deepseek-account' import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment' import { - catalogModelInfo, ModelListingCache, refuseListedModel, registerDeepSeekProvider, requestModelListing, resolveAdapterOptions, + catalogModelInfo, createModelListingCache, refuseListedModel, registerDeepSeekProvider, requestModelListing, resolveAdapterOptions, resolveListedCatalog, } from '@deepseek-ai/dsh-llm-deepseek' import type { DeepSeekRequestAuth, ListedCatalog, ListingView, ModelListing, ResolvedDeepSeekOptions } from '@deepseek-ai/dsh-llm-deepseek' @@ -50,15 +50,10 @@ export function apply(ctx: Context, config: Config): void { }, } } - const catalog = new ModelListingCache({ + const catalog = createModelListingCache(ctx, name, PROVIDER, { settings: () => settings().catalog, credential: async url => account()?.resolveToken(url), load: (url, token, signal) => requestModelListing(url, { [TOKEN_HEADER]: token }, signal), - now: () => Date.now(), - // ACP reserves stdout for protocol messages, including during successful catalog loads. - info: (message) => { console.error(LOG_TAG, message) }, - warn: (message) => { console.warn(LOG_TAG, message) }, - changed: () => { ctx.emit('llm/models-updated', PROVIDER) }, }) let loggedListing: ModelListing | undefined /** Resolve the catalog, reporting each listing's hidden entries and dropped badges once. */ @@ -103,5 +98,4 @@ export function apply(ctx: Context, config: Config): void { return offered.map(model => catalogModelInfo(provider, model, badges.get(model.id))) }, }) - ctx.effect(() => () => catalog.dispose(), 'llm-deepseek-account: model catalog') } diff --git a/packages/llm/llm-deepseek-api-key/src/index.ts b/packages/llm/llm-deepseek-api-key/src/index.ts index a67cf29b72..70f9468328 100644 --- a/packages/llm/llm-deepseek-api-key/src/index.ts +++ b/packages/llm/llm-deepseek-api-key/src/index.ts @@ -5,7 +5,7 @@ import type { LlmModelBadge } from '@deepseek-ai/dsh-llm' import type {} from '@deepseek-ai/cordis-plugin-loader' import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment' import { - catalogModelInfo, ModelListingCache, refuseListedModel, registerDeepSeekProvider, requestModelListing, resolveListedCatalog, + catalogModelInfo, createModelListingCache, refuseListedModel, registerDeepSeekProvider, requestModelListing, resolveListedCatalog, } from '@deepseek-ai/dsh-llm-deepseek' import type { CatalogRefusal, DeepSeekCatalogModel, ListingView, ModelListing } from '@deepseek-ai/dsh-llm-deepseek' import { Config, plainOptions, resolveAdapterOptions } from './config.ts' @@ -57,15 +57,10 @@ export function apply(ctx: Context, config: Config): void { throw error } } - const listings = new ModelListingCache({ + const listings = createModelListingCache(ctx, name, PROVIDER, { settings: () => options().listing, credential: async () => presentApiKey(options()), load: (url, apiKey, signal) => requestModelListing(url, { 'x-api-key': apiKey }, signal), - now: () => Date.now(), - // ACP reserves stdout for protocol messages, including during successful catalog loads. - info: (message) => { console.error(LOG_TAG, message) }, - warn: (message) => { console.warn(LOG_TAG, message) }, - changed: () => { ctx.emit('llm/models-updated', PROVIDER) }, }) let reportedListing: ModelListing | undefined /** Resolve the catalog, reporting each listing's hidden entries and dropped badges once. */ @@ -131,5 +126,4 @@ export function apply(ctx: Context, config: Config): void { if (next !== following) listings.invalidate() following = next }) - ctx.effect(() => () => listings.dispose(), 'llm-deepseek-api-key: model listing') } diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml index dc9eba8c22..0fa3c3fc6b 100644 --- a/packages/llm/llm-deepseek/README.i18n.yaml +++ b/packages/llm/llm-deepseek/README.i18n.yaml @@ -39,8 +39,8 @@ en: f13f24b504dcfb50 zh: 69b0ed9e2adf3394 /deepseek-ai-dsh-llm-deepseek/use-this-package/failures-and-recovery: - en: a7553dc4db7d8495 - zh: f59a1e111fbe798f + en: a8bbaf16292d4d61 + zh: e9ef390254446caf /deepseek-ai-dsh-llm-deepseek/understand-the-implementation: en: 1598ac8a8bb9b6bf zh: fdd089c5ae7722cd diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 39a88d63c6..beab26dbba 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -118,7 +118,7 @@ Successful Files responses must contain valid JSON. JSON decoding failures from Non-2xx responses fail with stable codes: `AUTH` (401/403), `QUOTA`, `RATE_LIMIT`, `CONTEXT_WINDOW_EXCEEDED`, `INVALID_REQUEST`, `SERVER`, and `HTTP_` otherwise; pre-response transport failures throw `TRANSPORT`, caller aborts throw `ABORTED`, and stream-idle expiry throws `TIMEOUT`. A request-extension field collision, or a rejected preparation from a directly supplied `prepareExtensions`, fails with `REQUEST_EXTENSION` and appends the cause chain to the message; a post-2xx acceptance failure is logged and the response still streams. A normalized-image rejection names every plausible attachment and its durable position when the provider does not identify a file id. Stale-file rejection removes every named mapping (or every mapping used by the attempt) in one upload-index update and permits one replacement model request. Protocol violations throw `STREAM_CLOSED` or `MALFORMED_RESPONSE`, and a terminal `stop` with no content blocks becomes `EMPTY_RESPONSE`, which the default retry policy retries. A request on the official route without an API key fails with `MISSING_CREDENTIAL`, and a malformed credential fails with `INVALID_CREDENTIAL` naming the reference to fix — never any part of the key. -Provider plugins own catalog availability. While its API key resolves, the API-key route serves its configured `models`, or the official model listing while `models` is the built-in catalog on the official endpoint; the account route serves its account listing while signed in (see the [API-key](../llm-deepseek-api-key/README.md) and [account](../llm-deepseek-account/README.md) providers). The transport supplies the bundled catalog, the catalog-entry field schemas, and capability resolution, plus the listing machinery both routes share: `requestModelListing`, the credential-scoped `ModelListingCache`, and `resolveListedCatalog` with its client-version gate, listed badges, and `refuseListedModel`; `catalogModelInfo` attaches a listed badge to the advertised entry. A provider may also supply `connect(signal)`, which resolves the connection for exact-model lookups and prepared calls after provider-owned work such as a catalog load, and `admit(connection, model)`, which refuses a model before network I/O. +Provider plugins own catalog availability. While its API key resolves, the API-key route serves its configured `models`, or the official model listing while `models` is the built-in catalog on the official endpoint; the account route serves its account listing while signed in (see the [API-key](../llm-deepseek-api-key/README.md) and [account](../llm-deepseek-account/README.md) providers). The transport supplies the bundled catalog, the catalog-entry field schemas, and capability resolution, plus the listing machinery both routes share: `requestModelListing`, the credential-scoped `ModelListingCache` (owned by `createModelListingCache` for automatic refresh, notifications, and plugin cleanup), and `resolveListedCatalog` with its client-version gate, listed badges, and `refuseListedModel`; `catalogModelInfo` attaches a listed badge to the advertised entry. A provider may also supply `connect(signal)`, which resolves the connection for exact-model lookups and prepared calls after provider-owned work such as a catalog load, and `admit(connection, model)`, which refuses a model before network I/O. ----- diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md index dd70ee871e..a80451e4fb 100644 --- a/packages/llm/llm-deepseek/README.zh.md +++ b/packages/llm/llm-deepseek/README.zh.md @@ -118,7 +118,7 @@ Files 模式通过 `maxRequestFilesBytes` 与 `maxImagesPerRequest` 限制保留 非 2xx 响应以稳定 code 失败:`AUTH`(401/403)、`QUOTA`、`RATE_LIMIT`、`CONTEXT_WINDOW_EXCEEDED`、`INVALID_REQUEST`、`SERVER` 以及其他情况的 `HTTP_`;响应前传输失败抛出 `TRANSPORT`,调用方中止抛出 `ABORTED`,流空闲超时抛出 `TIMEOUT`。请求扩展字段冲突,或直接提供的 `prepareExtensions` 拒绝准备时,使用 `REQUEST_EXTENSION` 失败,并在消息中附上原因链;2xx 后接受失败只记录告警,响应仍会继续流式返回。当提供方未指出 file id 时,规范化图片拒绝会列出所有可能附件及其持久位置。陈旧文件拒绝会在一次上传索引更新中删除全部点名映射(或该次尝试使用的全部映射),并允许一次替换模型请求。协议违规抛出 `STREAM_CLOSED` 或 `MALFORMED_RESPONSE`;不带内容块的终止 `stop` 变成 `EMPTY_RESPONSE`,默认重试策略会重试它。官方路由缺少 API Key 的请求以 `MISSING_CREDENTIAL` 失败;格式错误的凭据以 `INVALID_CREDENTIAL` 失败,并点名需要修复的引用——绝不包含密钥的任何部分。 -提供方插件负责目录可用性:API key 路由在能解析到 API key 时提供配置中的 `models`,在官方端点上且 `models` 为内置目录时改为提供官方模型列表;账号路由在登录时提供账号的模型列表(见 [API key](../llm-deepseek-api-key/README.zh.md) 与[账号](../llm-deepseek-account/README.zh.md)提供方)。传输层提供内置目录、目录条目字段的 schema 和能力解析,以及两条路由共用的列表机制:`requestModelListing`、按凭据区分的 `ModelListingCache`,以及带客户端版本门控和列表角标的 `resolveListedCatalog` 与 `refuseListedModel`;`catalogModelInfo` 把列表角标附到公布的条目上。提供方还可以提供 `connect(signal)`:在提供方自己的工作(例如加载目录)完成后,为精确模型查询和预备调用解析连接;以及 `admit(connection, model)`:在网络请求之前拒绝某个模型。 +提供方插件负责目录可用性:API key 路由在能解析到 API key 时提供配置中的 `models`,在官方端点上且 `models` 为内置目录时改为提供官方模型列表;账号路由在登录时提供账号的模型列表(见 [API key](../llm-deepseek-api-key/README.zh.md) 与[账号](../llm-deepseek-account/README.zh.md)提供方)。传输层提供内置目录、目录条目字段的 schema 和能力解析,以及两条路由共用的列表机制:`requestModelListing`、按凭据区分的 `ModelListingCache`(由 `createModelListingCache` 持有,负责自动刷新、通知与插件清理),以及带客户端版本门控和列表角标的 `resolveListedCatalog` 与 `refuseListedModel`;`catalogModelInfo` 把列表角标附到公布的条目上。提供方还可以提供 `connect(signal)`:在提供方自己的工作(例如加载目录)完成后,为精确模型查询和预备调用解析连接;以及 `admit(connection, model)`:在网络请求之前拒绝某个模型。 ----- diff --git a/packages/llm/llm-deepseek/src/host.ts b/packages/llm/llm-deepseek/src/host.ts index 69fc4fe0d5..c9e89cad8e 100644 --- a/packages/llm/llm-deepseek/src/host.ts +++ b/packages/llm/llm-deepseek/src/host.ts @@ -8,6 +8,32 @@ import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/d import { deepEqualJson } from '@deepseek-ai/dsh-util-values' import { DeepSeekAdapter } from './adapter.ts' import type { DeepSeekAdapterOptions, DeepSeekConnectionOptions } from './types.ts' +import { ModelListingCache, type ListingCacheDependencies } from './listing-cache.ts' + +/** + * Own a route's listing refreshes, stderr diagnostics, and catalog-change notifications. + * @param ctx - provider plugin lifetime. + * @param owner - plugin name used in diagnostics. + * @param provider - route whose catalog changes. + * @param dependencies - current configuration, credentials, and listing transport. + * @returns the cache, disposed with the provider after its active work settles. + */ +export function createModelListingCache( + ctx: Context, owner: string, provider: string, + dependencies: Pick, +): ModelListingCache { + const tag = `[${owner}]` + const cache = new ModelListingCache({ + ...dependencies, + now: () => Date.now(), + // ACP reserves stdout for protocol messages, including during successful catalog loads. + info: (message) => { console.error(tag, message) }, + warn: (message) => { console.warn(tag, message) }, + changed: () => { ctx.emit('llm/models-updated', provider) }, + }) + ctx.effect(() => () => cache.dispose(), `${owner}: model listing`) + return cache +} /** * Register one provider with request-local transport services and live retry policy. diff --git a/packages/llm/llm-deepseek/src/index.ts b/packages/llm/llm-deepseek/src/index.ts index 648de9206d..1c7ab36255 100644 --- a/packages/llm/llm-deepseek/src/index.ts +++ b/packages/llm/llm-deepseek/src/index.ts @@ -38,7 +38,7 @@ export type { DeepSeekUploadRecord } from './upload-index.ts' export type { RequestDefaults } from './types.ts' export { catalogModelInfo } from './model-info.ts' -export { registerDeepSeekProvider } from './host.ts' +export { createModelListingCache, registerDeepSeekProvider } from './host.ts' export { LISTING_TIMEOUT, modelListingURL, parseModelListing, requestModelListing } from './listing.ts' export type { ClientRequirement, ListedBadge, ListedCapabilities, ListedFacts, ListingEntry, ModelListing } from './listing.ts' export { DEFAULT_MODELS_MAX_AGE_MS, DEFAULT_MODELS_TIMEOUT_MS, modelListingConfigFields, resolveListingTiming } from './listing-config.ts'