fix(account): expire account credentials on inference HTTP 401

This commit is contained in:
lsdsjy committed 2026-09-22 16:12:01 +08:00
1 parent 8e92f17e9c
commit 3e4119ebdb
6 files changed
+17 -12

No files matched your search

+2 -2
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: 8019a8be1f0c8e1f00ae5573e350c3a7eaf1c721
README.zh.md: 86855c49ebeb9fd3c3ca578d52eefe7b6e4c8f92
README.md: 8d8e28faddf91026c249dbbfb0cfc183c815ae9d
README.zh.md: cee5d30a17a39d9e0613747db4b1a0035bef9c53
+1 -1
View File
@@ -86,7 +86,7 @@ Messages sends text, thinking, tool calls, and tool results as content blocks, r
`deepseek-official` resolves only its configured API-key reference. `deepseek-account` resolves only the stored grant from the [account provider](../../credentials/deepseek-account-platform/README.md), whose allowed `inferenceOrigin` defaults to `https://api.deepseek.com`. Neither route falls back to the other. Signing out removes the account grant and preserves API keys.
Messages and Files requests send account tokens as `x-dsh-auth-token` without a Bearer prefix; API keys use `x-api-key`. Neither credential mode follows redirects. HTTP 401 with the provider message `Authentication Fails (invalid dsh token)` becomes `ACCOUNT_TOKEN_INVALID`. On the account route, it removes the request token only if it still matches the stored login, then publishes account expiry and UI sign-out. Generic authentication errors and API-key routes do not remove account credentials.
Messages and Files requests send account tokens as `x-dsh-auth-token` without a Bearer prefix; API keys use `x-api-key`. Neither credential mode follows redirects. HTTP 401 on a request authenticated with an account token becomes `ACCOUNT_TOKEN_INVALID`, regardless of the response body. It removes that request token only if it still matches the stored login, then publishes account expiry and UI sign-out. API-key routes and other HTTP statuses do not remove account credentials.
### Streaming with thinking and images
+1 -1
View File
@@ -86,7 +86,7 @@ Messages 以内容块发送文本、思考、工具调用和工具结果,以 `
`deepseek-official` 仅解析配置的 API Key 引用。`deepseek-account` 仅解析[账号提供者](../../credentials/deepseek-account-platform/README.zh.md)保存的授权,其允许的 `inferenceOrigin` 默认为 `https://api.deepseek.com`。两条路由均不回退到另一凭证。退出登录删除账号授权,保留 API Key。
Messages 和 Files 请求通过 `x-dsh-auth-token` 发送账号 token,不加 Bearer 前缀;API Key 使用 `x-api-key`。两种凭据模式均拒绝重定向。HTTP 401 且提供方消息为 `Authentication Fails (invalid dsh token)` 时归类为 `ACCOUNT_TOKEN_INVALID`。账号路由仅在请求 token 仍匹配已存登录凭据时删除它,并发布账号过期状态和 UI 退登通知。普通鉴权错误和 API Key 路由不会删除账号凭据。
Messages 和 Files 请求通过 `x-dsh-auth-token` 发送账号 token,不加 Bearer 前缀;API Key 使用 `x-api-key`。两种凭据模式均拒绝重定向。使用账号 token 鉴权的请求返回 HTTP 401 时归类为 `ACCOUNT_TOKEN_INVALID`,不依赖响应体内容。仅当该请求的 token 仍匹配已存登录凭据时才删除它,并发布账号过期状态和 UI 退登通知。API Key 路由和其他 HTTP 状态码不会删除账号凭据。
### 带 thinking 与图片的流式调用
+2 -1
View File
@@ -139,7 +139,8 @@ export class DeepSeekAdapter extends LlmAdapter {
if (await files.retry(detail)) continue
const failure = providerError(raw, response.status, response.headers)
const message = files.errorMessage(response.status, failure.message, detail)
throw new LlmError(message, failure.code, { ...failure.failure, cause: new Error(text) })
const code = accountToken !== undefined && response.status === 401 ? 'ACCOUNT_TOKEN_INVALID' : failure.code
throw new LlmError(message, code, { ...failure.failure, cause: new Error(text) })
}
await extensions.accept()
if (response.body === null) throw new LlmError('DeepSeek Messages returned no response body', 'EMPTY_RESPONSE')
+1 -2
View File
@@ -26,8 +26,7 @@ export function providerError(raw: unknown, status: number | undefined, headers?
const type = typeof error.type === 'string' ? error.type : ''
const detail = `${type} ${typeof error.code === 'string' ? error.code : ''} ${message}`
let code: string
if (status === 401 && message === 'Authentication Fails (invalid dsh token)') code = 'ACCOUNT_TOKEN_INVALID'
else if (status === 401 || status === 403 || ['authentication_error', 'permission_error'].includes(type)) code = 'AUTH'
if (status === 401 || status === 403 || ['authentication_error', 'permission_error'].includes(type)) code = 'AUTH'
else if (isQuotaExceededError(detail) || status === 402) code = 'QUOTA'
else if (status === 429 || type === 'rate_limit_error') code = 'RATE_LIMIT'
else if (isContextWindowExceededError(detail)) code = 'CONTEXT_WINDOW_EXCEEDED'
@@ -277,16 +277,21 @@ describe('Cordis provider composition', () => {
return { ctx, http }
}
it.each(['deepseek-account', 'deepseek-official'])('reports the rejected account token only for %s', async (provider) => {
it.each(['deepseek-account', 'deepseek-official'].flatMap(provider => [
{ provider, body: JSON.stringify({ error: { type: 'authentication_error', message: 'API key is invalid' } }) },
{ provider, body: JSON.stringify({ error: { message: 'Authentication Fails (invalid dsh token)' } }) },
{ provider, body: 'Unauthorized' },
{ provider, body: '' },
]))('handles $provider HTTP 401 independently of the response body ($body)', async ({ provider, body }) => {
const { ctx } = await boot((response) => {
response.writeHead(401, { 'content-type': 'application/json' })
response.end(JSON.stringify({ error: { type: 'authentication_error', message: 'Authentication Fails (invalid dsh token)' } }))
response.end(body)
})
const rejectToken = vi.fn(async (_token: string) => {})
ctx.provide('deepseekAccount', { resolveToken: async (_url: string): Promise<string | undefined> => 'fixture-token',
rejectToken: (token: string): Promise<void> => rejectToken(token) } as DeepSeekAccount)
expect((await chunks(ctx.llm.stream(options({ provider })))).at(-1)).toMatchObject({
type: 'finish', reason: { kind: 'error', failure: { code: 'ACCOUNT_TOKEN_INVALID' } },
type: 'finish', reason: { kind: 'error', failure: { code: provider === 'deepseek-account' ? 'ACCOUNT_TOKEN_INVALID' : 'AUTH' } },
})
expect(rejectToken.mock.calls).toEqual(provider === 'deepseek-account' ? [['fixture-token']] : [])
})
@@ -318,9 +323,9 @@ describe('Cordis provider composition', () => {
})
})
it('does not remove account credentials for a generic authentication error', async () => {
it('does not remove account credentials for HTTP 403', async () => {
const { ctx } = await boot((response) => {
response.writeHead(401)
response.writeHead(403)
response.end(JSON.stringify({ error: { type: 'authentication_error', message: 'API key is invalid' } }))
})
const rejectToken = vi.fn(async (_token: string) => {})