fix(account): expire account credentials on inference HTTP 401
This commit is contained in:
1 parent
8e92f17e9c
commit
3e4119ebdb
6 files changed
+17
-12
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 与图片的流式调用
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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) => {})
|
||||
|
||||
Reference in new issue
Block a user