refactor(models): share listing cache lifecycle wiring

This commit is contained in:
Yichen Jiang committed 2026-10-27 15:01:54 +08:00
1 parent 8394a200b9
commit a36745f6ee
7 files changed
+35 -21

No files matched your search

@@ -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')
}
@@ -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')
}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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_<status>` 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.
-----
+1 -1
View File
@@ -118,7 +118,7 @@ Files 模式通过 `maxRequestFilesBytes` 与 `maxImagesPerRequest` 限制保留
非 2xx 响应以稳定 code 失败:`AUTH`(401/403)、`QUOTA`、`RATE_LIMIT`、`CONTEXT_WINDOW_EXCEEDED`、`INVALID_REQUEST`、`SERVER` 以及其他情况的 `HTTP_<status>`;响应前传输失败抛出 `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)`:在网络请求之前拒绝某个模型。
-----
+26
View File
@@ -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<ListingCacheDependencies, 'settings' | 'credential' | 'load'>,
): 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.
+1 -1
View File
@@ -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'