fix(translator): read the account route's model overrides instead of its removed catalog

The account route no longer accepts a models catalog, so paid Flash
translation compares Account modelOverrides (Official keeps models) when
it checks that the owner's model configuration is unchanged before
dispatch. The paid fixture and the Web e2e serve the account GET /models
listing instead of configuring models on the account entry.
This commit is contained in:
Yichen Jiang
2026-10-12 14:53:15 +08:00
parent 3a0f4976ad
commit 54a7bdec23
11 changed files with 60 additions and 20 deletions
+9 -2
View File
@@ -58,6 +58,13 @@ async function messageBody(request: IncomingMessage): Promise<Record<string, unk
return body as Record<string, unknown>
}
/** Account model listing at the Messages origin; the version name is arbitrary because selection uses the id. */
function listing(response: ServerResponse): void {
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify({ object: 'list', data: [{ id: 'deepseek-flash', name: 'Native Flash arbitrary future version',
api_capabilities: { anthropic_messages: {} } }] }))
}
/** Complete external native Messages SSE; no Host method or adapter is replaced. */
function reply(response: ServerResponse, text: string): void {
response.writeHead(200, { 'content-type': 'text/event-stream' })
@@ -81,6 +88,7 @@ it.skipIf(MODE === 'record')('reuses saved native Account Flash translations acr
let holdNext = false
async function respond(request: IncomingMessage, response: ServerResponse): Promise<void> {
const path = new URL(request.url ?? '/', 'http://native.test').pathname
if (path === '/models') { listing(response); return }
if (path !== '/anthropic/v1/messages') throw new Error(`Unexpected external translation request: ${path}`)
const auditSessionId = resources.sessionId
if (auditSessionId === undefined || resources.scaffold === undefined) throw new Error('Paid I/O preceded the recorded Session')
@@ -123,8 +131,7 @@ it.skipIf(MODE === 'record')('reuses saved native Account Flash translations acr
const origin = `http://127.0.0.1:${address.port}`
const overlay = join(root, 'paid-translation.patch.yml')
await writeFile(overlay, JSON.stringify([
{ id: 'llm-deepseek-account', config: { baseURL: `${origin}/anthropic`, reasoningEffort: 'high',
models: [{ id: 'deepseek-flash', name: 'Native Flash arbitrary future version' }] } },
{ id: 'llm-deepseek-account', config: { baseURL: `${origin}/anthropic`, reasoningEffort: 'high' } },
{ id: 'translator', config: { bingEndpoint: `${origin}/unexpected-free`, googleEndpoint: `${origin}/unexpected-free`,
deepseekTimeoutMs: 120_000 } },
]))
+2 -2
View File
@@ -9,8 +9,8 @@
en: 485abf6107c73e68
zh: 78a274d488274035
/text-translation/failures-and-data-handling:
en: cf92a1920650888c
zh: 3e8d3aa244cc8d46
en: 436e55e924e3ad1e
zh: c8548fda1e86b078
/text-translation/persistent-translations:
en: 40d175fbe66fd4cd
zh: bff18511a8d386e9
+1 -1
View File
@@ -14,7 +14,7 @@ The service exposes its configured `maxTextChars`, measured in UTF-16 code units
`TranslationError` carries a `TranslationErrorCode` distinguishing text limits, HTTP failures, invalid responses, response limits, owned deadlines, and request failures. `TRANSLATION_SESSION_REQUIRED` rejects a missing persistence backend or Session; `TRANSLATION_SESSION_INACTIVE` rejects an uncached inactive Session before provider dispatch; `TRANSLATION_STORAGE_ERROR` rejects invalid saved records or failed durability. Provider diagnostics omit submitted text and response bodies. Failed calls do not select another provider. Responses are validated as external JSON and redirects are rejected.
`TRANSLATION_UNAVAILABLE` rejects a missing active built-in owner or registered route, unconfigured credentials, absent exact `deepseek-flash` catalog membership, an owner/model-list/official credential-reference change before dispatch, provider withdrawal, or a prepared call whose reasoning effort is not `off`. A native `finish` with reason `error` maps to `TRANSLATION_REQUEST_FAILED`; a missing or other non-`stop` finish maps to `TRANSLATION_INVALID_RESPONSE`.
`TRANSLATION_UNAVAILABLE` rejects a missing active built-in owner or registered route, unconfigured credentials, absent exact `deepseek-flash` catalog membership, an owner, model-configuration (Official `models` or Account `modelOverrides`), or Official credential-reference change before dispatch, provider withdrawal, or a prepared call whose reasoning effort is not `off`. A native `finish` with reason `error` maps to `TRANSLATION_REQUEST_FAILED`; a missing or other non-`stop` finish maps to `TRANSLATION_INVALID_RESPONSE`.
Google receives source text in a form POST body; Bing receives it in a JSON POST body. Both are remote services. Their anonymous endpoints provide no supported developer availability or quota guarantee. Regional connectivity must be verified on the user's network.
+1 -1
View File
@@ -14,7 +14,7 @@
`TranslationError` 的 `TranslationErrorCode` 区分文本超限、HTTP 失败、无效响应、响应超限、服务自身超时与请求失败。`TRANSLATION_SESSION_REQUIRED` 拒绝缺失的持久化后端或 Session,`TRANSLATION_SESSION_INACTIVE` 在调用 Provider 前拒绝未激活且缓存未命中的 Session,`TRANSLATION_STORAGE_ERROR` 拒绝无效保存记录或持久化失败。Provider 错误信息省略提交的文本与响应体。失败时不选择其他 Provider。响应作为外部 JSON 校验,重定向被拒绝。
`TRANSLATION_UNAVAILABLE` 拒绝缺失的活跃内置所有者或已注册路由、未配置的凭据、目录中不存在精确的 `deepseek-flash`、派发前所有者/模型列表/官方凭据引用变化、提供方卸载,以及准备后的调用推理强度不是 `off` 的情况。原生 `finish` 的原因为 `error` 时映射为 `TRANSLATION_REQUEST_FAILED`;缺失 finish 或其他非 `stop` 原因映射为 `TRANSLATION_INVALID_RESPONSE`。
`TRANSLATION_UNAVAILABLE` 拒绝缺失的活跃内置所有者或已注册路由、未配置的凭据、目录中不存在精确的 `deepseek-flash`、派发前所有者、模型配置(官方路由的 `models` 或账号路由的 `modelOverrides`)或官方凭据引用变化、提供方卸载,以及准备后的调用推理强度不是 `off` 的情况。原生 `finish` 的原因为 `error` 时映射为 `TRANSLATION_REQUEST_FAILED`;缺失 finish 或其他非 `stop` 原因映射为 `TRANSLATION_INVALID_RESPONSE`。
Google 在 form POST 请求体中接收原文,Bing 在 JSON POST 请求体中接收原文。两者都是远端服务。免登录端点不保证开发者 API 的可用性或免费额度。区域连通性须在用户网络上验证。
@@ -18,8 +18,8 @@
en: a8a0b2481c0966c3
zh: aedb4a655c2c96c2
/deepseek-ai-dsh-experimental-translator/understand-the-implementation:
en: 8fa99924e5a2993e
zh: 269b0e9546109251
en: 26bdde8357a1bb43
zh: 727c36c494dc644a
/deepseek-ai-dsh-experimental-translator/further-exploration:
en: 17792a9fe5429633
zh: b51e5e3c30b0e14d
+1 -1
View File
@@ -64,7 +64,7 @@ Storage reads validate translation payloads and request/result references. Only
`TranslationError.code` distinguishes input limits, HTTP failures, response limits, invalid provider responses, transport failures, missing durable Sessions, storage failures, unavailable native routes, and the service's `TRANSLATION_TIMEOUT` deadline. `TRANSLATION_SESSION_INACTIVE` rejects an uncached inactive Session before any provider call. Diagnostics include no submitted text or provider error body. `translate()` always returns a Promise and rejects admission or provider failures. Caller cancellation and service disposal preserve their original abort reasons, including caller-owned timeout reasons. Queued cancellation rejects promptly; unloading aborts accepted requests and waits for their underlying storage and network work to settle. Input limits apply in both `resolve()` and `translate()`.
`TRANSLATION_UNAVAILABLE` rejects a missing active built-in owner or registered route, unconfigured credentials, absent exact `deepseek-flash` catalog membership, an owner/model-list/official credential-reference change before dispatch, provider withdrawal, or a prepared call whose reasoning effort is not `off`. Eligibility and thinking checks run before dispatch, with eligibility checked again after request durability. A native `finish` with reason `error` maps to `TRANSLATION_REQUEST_FAILED`; a missing or other non-`stop` finish maps to `TRANSLATION_INVALID_RESPONSE`.
`TRANSLATION_UNAVAILABLE` rejects a missing active built-in owner or registered route, unconfigured credentials, absent exact `deepseek-flash` catalog membership, an owner, model-configuration (Official `models` or Account `modelOverrides`), or Official credential-reference change before dispatch, provider withdrawal, or a prepared call whose reasoning effort is not `off`. Eligibility and thinking checks run before dispatch, with eligibility checked again after request durability. A native `finish` with reason `error` maps to `TRANSLATION_REQUEST_FAILED`; a missing or other non-`stop` finish maps to `TRANSLATION_INVALID_RESPONSE`.
</details>
@@ -64,7 +64,7 @@ Google 接收使用 `gtx` 客户端的表单 POST 请求;Bing 通过 Microsoft
`TranslationError.code` 区分输入限制、HTTP 失败、响应限制、无效提供方响应、传输失败、缺少持久 Session、存储失败、不可用的原生路由以及服务自身的 `TRANSLATION_TIMEOUT` 超时。未激活 Session 缓存未命中时,在调用任何提供方之前以 `TRANSLATION_SESSION_INACTIVE` 拒绝。诊断不包含提交的文本或提供者错误正文。`translate()` 始终返回 Promise,准入或提供者失败都会使其拒绝。调用者取消和服务卸载保留原始中止原因,包括调用者自身的超时原因。排队期间取消会立即返回拒绝;卸载会取消已接纳的请求,并等待底层存储和网络工作结束。`resolve()` 和 `translate()` 都执行输入限制。
`TRANSLATION_UNAVAILABLE` 拒绝缺失的活跃内置所有者或已注册路由、未配置的凭据、目录中不存在精确的 `deepseek-flash`、派发前所有者/模型列表/官方凭据引用变化、提供方卸载,以及准备后的调用推理强度不是 `off` 的情况。资格与思考检查在派发前执行,请求持久化之后会再次检查资格。原生 `finish` 的原因为 `error` 时映射为 `TRANSLATION_REQUEST_FAILED`;缺失 finish 或其他非 `stop` 原因映射为 `TRANSLATION_INVALID_RESPONSE`。
`TRANSLATION_UNAVAILABLE` 拒绝缺失的活跃内置所有者或已注册路由、未配置的凭据、目录中不存在精确的 `deepseek-flash`、派发前所有者、模型配置(官方路由的 `models` 或账号路由的 `modelOverrides`)或官方凭据引用变化、提供方卸载,以及准备后的调用推理强度不是 `off` 的情况。资格与思考检查在派发前执行,请求持久化之后会再次检查资格。原生 `finish` 的原因为 `error` 时映射为 `TRANSLATION_REQUEST_FAILED`;缺失 finish 或其他非 `stop` 原因映射为 `TRANSLATION_INVALID_RESPONSE`。
</details>
@@ -4,7 +4,7 @@ import type {} from '@deepseek-ai/cordis-plugin-loader'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
import { BlockAssembler, ReasoningEffortId, type LlmRuntime, type GenerateOptions, type RequestUserInput, type FinishReason, type TextBlock } from '@deepseek-ai/dsh-llm'
import type { Config as SharedNativeConfig } from '@deepseek-ai/dsh-llm-deepseek'
import type { Config as AccountNativeConfig } from '@deepseek-ai/dsh-llm-deepseek-account'
import type { Config as OfficialNativeConfig } from '@deepseek-ai/dsh-llm-deepseek-api-key'
import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
import { deepFreeze } from '@deepseek-ai/dsh-util-values'
@@ -19,10 +19,16 @@ const OWNER_PACKAGES = { 'deepseek-account': '@deepseek-ai/dsh-llm-deepseek-acco
interface NativeOwnerFields { llm: LlmRuntime; fiber: Fiber }
type NativeOwner = NativeOwnerFields & (
| { provider: 'deepseek-account'; config: SharedNativeConfig }
| { provider: 'deepseek-account'; config: AccountNativeConfig }
| { provider: 'deepseek-official'; config: OfficialNativeConfig }
)
type EligibleOwner = NativeOwner & { models: ReturnType<SharedNativeConfig['models']['get']>; apiKeyRef?: string }
/** Configured model input of an owner: Account overrides over its listing, or the Official catalog. */
type ModelConfig = ReturnType<AccountNativeConfig['modelOverrides']['get']> | ReturnType<OfficialNativeConfig['models']['get']>
type EligibleOwner = NativeOwner & { modelConfig: ModelConfig; apiKeyRef?: string }
function modelConfigOf(owner: NativeOwner): ModelConfig {
return owner.provider === 'deepseek-account' ? owner.config.modelOverrides.get() : owner.config.models.get()
}
/**
* Identify fixed native translation semantics without inspecting credentials or a model catalog.
@@ -66,14 +72,14 @@ function nativeOwner(ctx: Context, provider: PaidTranslationProvider): NativeOwn
const entry = [...loader.entries()].find(row => row.options.id === directory.settingsNs && row.options.name === OWNER_PACKAGES[provider])
if (entry?.fiber?.state !== FiberState.ACTIVE) return undefined
return provider === 'deepseek-account'
? { llm, fiber: entry.fiber, provider, config: entry.fiber.config as SharedNativeConfig }
? { llm, fiber: entry.fiber, provider, config: entry.fiber.config as AccountNativeConfig }
: { llm, fiber: entry.fiber, provider, config: entry.fiber.config as OfficialNativeConfig }
}
function unchanged(ctx: Context, provider: PaidTranslationProvider, previous: EligibleOwner): NativeOwner | undefined {
const current = nativeOwner(ctx, provider)
if (current === undefined || current.fiber !== previous.fiber
|| current.config.models.get() !== previous.models
|| modelConfigOf(current) !== previous.modelConfig
|| (current.provider === 'deepseek-official' && current.config.apiKeyEnv.get() !== previous.apiKeyRef)) return undefined
return current
}
@@ -83,7 +89,7 @@ async function eligible(ctx: Context, provider: PaidTranslationProvider, signal:
signal.throwIfAborted()
const owner = nativeOwner(ctx, provider)
if (owner === undefined) return undefined
const models = owner.config.models.get()
const modelConfig = modelConfigOf(owner)
const apiKeyRef = owner.provider === 'deepseek-official' ? owner.config.apiKeyEnv.get() : undefined
if (apiKeyRef !== undefined) {
const credentials = ctx.get('credentials')
@@ -94,7 +100,7 @@ async function eligible(ctx: Context, provider: PaidTranslationProvider, signal:
const catalog = await awaitMetadata(() => owner.llm.listModels(provider), signal, track)
signal.throwIfAborted()
if (!catalog.some(model => model.id === MODEL)) return undefined
const accepted = { ...owner, models, ...apiKeyRef === undefined ? {} : { apiKeyRef } }
const accepted = { ...owner, modelConfig, ...apiKeyRef === undefined ? {} : { apiKeyRef } }
return unchanged(ctx, provider, accepted) === undefined ? undefined : accepted
}
@@ -1,6 +1,7 @@
/** Paid selection, independent native requests, audit-before-I/O and cancellation. */
import { Context, FiberState } from '@deepseek-ai/cordis'
import { ReasoningEffortId, createUserMessage, createAssistantMessage, type PreparedLlmCall } from '@deepseek-ai/dsh-llm'
import { Config as AccountConfig } from '@deepseek-ai/dsh-llm-deepseek-account'
import { Config as OfficialConfig } from '@deepseek-ai/dsh-llm-deepseek-api-key'
import { pluginRecordOf, SessionId } from '@deepseek-ai/dsh-session'
import { TimeoutReason } from '@deepseek-ai/dsh-timeout'
@@ -282,6 +283,24 @@ describe('native Flash translation', () => {
expect(b.session?.snapshotEvents()).toEqual([])
})
it('rejects an Account model-override change during async preparation before audit or dispatch', async () => {
const b = await paidFixture(cleanups), entered = Promise.withResolvers<undefined>(), release = Promise.withResolvers<undefined>()
const original = b.ctx.llm.prepareCall.bind(b.ctx.llm)
vi.spyOn(b.ctx.llm, 'prepareCall').mockImplementationOnce(async (config, signal) => {
const call = await original(config, signal)
entered.resolve(undefined); await release.promise; return call
})
const rejected = expect(b.translate('deepseek-account')).rejects.toMatchObject({ code: 'TRANSLATION_UNAVAILABLE' })
await entered.promise
const owner = [...b.ctx.loader.entries()].find(entry => entry.options.id === 'account-custom-id')!
const config = owner.fiber!.config as ReturnType<typeof AccountConfig>
config.modelOverrides = AccountConfig({ modelOverrides: { 'deepseek-flash': { maxTokens: 4096 } } }).modelOverrides
release.resolve(undefined)
await rejected
expect(b.requests).toEqual([])
expect(b.session?.snapshotEvents()).toEqual([])
})
it.each(['withdrawn', 'catalog', 'credential'])('rejects a native route %s by an audit observer before dispatch', async (change) => {
const b = await paidFixture(cleanups)
const owner = [...b.ctx.loader.entries()].find(entry => entry.options.id === 'official-custom-id')!
@@ -49,6 +49,7 @@ export function sse(events: Record<string, unknown>[]): string {
export interface FixtureOptions {
translator?: Partial<Config>
officialModels?: { id: string; name: string }[]
/** Entries of the account `GET /models` listing. */
accountModels?: { id: string; name: string }[]
officialName?: string
withCredentials?: boolean
@@ -82,6 +83,12 @@ export async function paidFixture(cleanups: Array<() => Promise<void>>, options:
let auditId = SessionId('paid-translation')
const server = createServer((request, response) => {
const work = (async () => {
if (request.method === 'GET' && request.url === '/models') {
const listed = options.accountModels ?? [{ id: 'deepseek-flash', name: 'Another arbitrary Flash version' }]
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify({ object: 'list', data: listed.map(model => ({ ...model, api_capabilities: { anthropic_messages: {} } })) }))
return
}
const chunks: Buffer[] = []
for await (const chunk of request) {
if (!(chunk instanceof Buffer)) throw new Error('Expected HTTP body bytes')
@@ -125,9 +132,7 @@ export async function paidFixture(cleanups: Array<() => Promise<void>>, options:
{ id: 'official-custom-id', name: options.officialName ?? '@deepseek-ai/dsh-llm-deepseek-api-key', disabled: !withNativeOwners, config: {
baseURL, apiKeyEnv: 'PAID_TRANSLATION_FIXTURE_KEY', models: options.officialModels ?? [{ id: 'deepseek-flash', name: 'Flash version-independent name' }],
} },
{ id: 'account-custom-id', name: '@deepseek-ai/dsh-llm-deepseek-account', disabled: !withNativeOwners, config: {
baseURL, models: options.accountModels ?? [{ id: 'deepseek-flash', name: 'Another arbitrary Flash version' }],
} },
{ id: 'account-custom-id', name: '@deepseek-ai/dsh-llm-deepseek-account', disabled: !withNativeOwners, config: { baseURL } },
{ id: 'translator', name: '@deepseek-ai/dsh-experimental-translator', config: translator },
...options.withSessionQuery === true ? [
{ id: 'query', name: '@deepseek-ai/dsh-session-query-sqlite', config: { path: join(root, 'query.sqlite'), openAt: 'never' } },
@@ -38,6 +38,9 @@
{
"path": "../../llm/llm-deepseek-api-key"
},
{
"path": "../../llm/llm-deepseek-account"
},
{
"path": "../../credentials/credentials"
},