test(llm): cover the account listing and gate through the Loader composition

This commit is contained in:
Yichen Jiang committed 2026-09-24 13:38:08 +08:00
1 parent 7653ca66ab
commit 7755d97dba
2 files changed
+36 -1

No files matched your search

@@ -392,6 +392,27 @@ describe('Cordis provider composition', () => {
})
})
it('serves the account listing through the Loader composition and refuses models this build cannot serve', async () => {
const { ctx, http } = await boot(undefined, { data: [
{
id: 'deepseek-flash', name: 'DeepSeek-V4.1-Flash', context_window: 1_048_576, max_output_tokens: 100_000,
input_modalities: ['text', 'image'], output_modalities: ['text'],
api_capabilities: { anthropic_messages: { system_prompt_update: 'in-history' } },
},
{
id: 'deepseek-v5', name: 'DeepSeek-V5', api_capabilities: { anthropic_messages: {} },
client_requirements: { 'deepseek-harness': { min_version: '99.0.0' } },
},
] })
ctx.provide('deepseekAccount', { resolveToken: async (_url: string): Promise<string | undefined> => 'fixture-token',
rejectToken: async (_token: string): Promise<void> => {} } as DeepSeekAccount)
expect((await ctx.llm.listModels('deepseek-account')).map(model => [model.id, model.name])).toEqual([['deepseek-flash', 'DeepSeek-V4.1-Flash']])
await expect(ctx.llm.prepareCall({ provider: 'deepseek-account', model: 'deepseek-v5' }))
.rejects.toMatchObject({ code: 'CLIENT_UPGRADE_REQUIRED' })
await assemble(ctx.llm.stream(options({ provider: 'deepseek-account', model: 'deepseek-flash' })), 'deepseek-flash')
expect(http.requests.map(request => [request.path, request.body.max_tokens])).toEqual([['/anthropic/v1/messages', 100_000]])
})
it('does not remove account credentials for HTTP 403', async () => {
const { ctx } = await boot((response) => {
response.writeHead(403)
+15 -1
View File
@@ -46,12 +46,26 @@ export function adapter(config: Config = {}) {
const files = new DeepSeekFileStore()
return new DeepSeekAdapter({ options: () => resolveAdapterOptions(config), resolveAuth: () => Promise.resolve({ headers: { 'x-api-key': 'test-key' } }), resolveUserId: () => 'test-user' as AnonymousUserId, resolveAttachments: () => undefined, resolveImageAccess: () => undefined, resolveFiles: () => files, prepareExtensions })
}
export async function server(reply: (response: ServerResponse, count: number) => void = response => response.end(sse(textEvents))) {
/**
* Loopback Messages endpoint recording each model request.
* @param reply - answers each Messages request.
* @param listing - JSON body served for `GET /models`; omission leaves that request unanswered like any other.
* @returns the endpoint base URL, recorded requests, and teardown.
*/
export async function server(
reply: (response: ServerResponse, count: number) => void = response => response.end(sse(textEvents)),
listing?: unknown,
) {
const requests: { path: string; headers: IncomingHttpHeaders; body: Record<string, unknown> }[] = []
const http = createServer((request, response) => {
void handle(request, response).catch((error: unknown) => response.destroy(error as Error))
})
async function handle(request: import('node:http').IncomingMessage, response: ServerResponse) {
if (listing !== undefined && request.method === 'GET' && request.url === '/models') {
response.setHeader('content-type', 'application/json')
response.end(JSON.stringify(listing))
return
}
const parts: Buffer[] = []
for await (const part of request as AsyncIterable<Buffer>) parts.push(part)
requests.push({ path: request.url!, headers: request.headers, body: object(JSON.parse(Buffer.concat(parts).toString())) })