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:
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()
|
||||
})
|
||||
Reference in new issue
Block a user