fix(llm-deepseek-api-key): report credential failures on the model request

On the official endpoint, `connect` waited for the listing view, whose
credential lookup rethrew a malformed key or a credential-store failure
before the agent loop recorded the prompt. A failed lookup now resolves
the connection from the current listing view, so `resolveAuth` reports
the failure on the model request and the turn keeps the prompt and the
failed attempt. A caller abort still rejects.
This commit is contained in:
Yichen Jiang committed 2026-10-22 12:01:37 +08:00
1 parent ef2d457a53
commit 542e0530ea
3 files changed
+66 -1

No files matched your search

@@ -90,7 +90,15 @@ export function apply(ctx: Context, config: Config): void {
},
connect: async (signal) => {
const connection = options()
return connectionOf(connection, connection.listing === undefined ? undefined : await listings.view(connection.listing, signal))
if (connection.listing === undefined) return connectionOf(connection, undefined)
let view: ListingView
try { view = await listings.view(connection.listing, signal) }
catch (error) {
if (signal?.aborted === true) throw error
// A credential lookup failure is reported by `resolveAuth` on the model request, after the turn records the prompt.
view = listings.current()
}
return connectionOf(connection, view)
},
resolveAuth: async connection => ({ headers: { 'x-api-key': await resolveApiKey(connection) } }),
admit: (connection, model) => { refuseListedModel(model, connection.refusals.get(model)) },
@@ -100,6 +100,21 @@ it('refuses listed models this build cannot serve before any inference request',
expect(requests).toHaveLength(1)
})
it('lets a caller abandon the first listing wait', async () => {
const started = Promise.withResolvers<undefined>()
vi.spyOn(globalThis, 'fetch').mockImplementation((_input, init) => new Promise((_resolve, reject) => {
started.resolve(undefined)
init?.signal?.addEventListener('abort', () => { reject(init.signal?.reason) }, { once: true })
}))
const { ctx } = await host()
await ctx.plugin(ApiKey, {})
const caller = new AbortController()
const pending = ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-flash', caller.signal)
await started.promise
caller.abort(new Error('caller left'))
await expect(pending).rejects.toThrow('caller left')
})
it.each([[{}], [{ models: [{ id: 'custom' }] }]])('hides the route without a key and requests nothing for %j', async (config) => {
const requests = officialListing(listing(FLASH))
const { ctx } = await host('')
@@ -0,0 +1,42 @@
/** Turns on the official endpoint report credential failures on the model request, after the prompt is recorded. */
import { afterEach, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmRuntime, { createUserMessage } from '@deepseek-ai/dsh-llm'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import * as NativeToolExecutor from '@deepseek-ai/dsh-tool-executor-native'
import * as DeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
const contexts: Context[] = []
afterEach(async () => {
for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
vi.restoreAllMocks()
vi.unstubAllEnvs()
})
it('records the prompt and fails the request when the official endpoint has a malformed key', async () => {
vi.stubEnv('DEEPSEEK_API_KEY', 'invalid\nheader')
const fetch = vi.spyOn(globalThis, 'fetch')
const ctx = new Context()
contexts.push(ctx)
for (const plugin of [
LlmRuntime, SessionStore, SessionProjectionRegistry, SystemPrompt, ToolRuntime, NativeToolExecutor, AgentRegistry,
]) {
await ctx.plugin(plugin)
}
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(DeepSeek, {})
const agent = await ctx.agentLoop.create(SessionId('malformed-official-key'), { provider: 'deepseek-official', model: 'deepseek-flash' })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } }))
await agent.whenIdle()
const events = agent.session.snapshotEvents()
expect(events.map(event => event.type)).toContain('user/message')
expect(events.find(event => event.type === 'assistant/attempt')?.data).toMatchObject({
stream: [{ chunk: { type: 'finish', reason: { kind: 'error', failure: { code: 'INVALID_CREDENTIAL' } } } }],
})
expect(fetch).not.toHaveBeenCalled()
})