|
|
@@ -2,14 +2,21 @@
|
|
|
* Real Messages round trips use the official root and require credentials.
|
|
|
* System-update checks additionally require DEEPSEEK_IN_HISTORY_MODEL.
|
|
|
*/
|
|
|
+import { randomUUID } from 'node:crypto'
|
|
|
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
|
import { tmpdir } from 'node:os'
|
|
|
import { join } from 'node:path'
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
import { Context, LoggerLevel } from '@deepseek-ai/cordis'
|
|
|
+import Loader from '@deepseek-ai/cordis-plugin-loader'
|
|
|
+import AgentRegistry from '@deepseek-ai/dsh-agent'
|
|
|
import LocalAttachments from '@deepseek-ai/dsh-attachment-local'
|
|
|
-import LlmRuntime, { createSystemMessage, createToolResultMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
|
|
+import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
|
|
|
+import LlmRuntime, { BlockAssembler, createSystemMessage, createToolResultMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
|
|
import type { Message } from '@deepseek-ai/dsh-llm'
|
|
|
+import * as PluginPackageInventoryDeepSeek from '@deepseek-ai/dsh-plugin-package-inventory-deepseek'
|
|
|
+import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
|
|
+import * as SessionLogDeepSeek from '@deepseek-ai/dsh-session-log-deepseek'
|
|
|
import * as Messages from '../../src/index.ts'
|
|
|
import { DeepSeekFilesClient, MESSAGES_FILES_BETA } from '../../src/common/files-api.ts'
|
|
|
import { assemble, options, user } from './helpers.ts'
|
|
|
@@ -29,7 +36,6 @@ async function boot(models?: Messages.Config['models']) {
|
|
|
cleanups.push(() => ctx.fiber.dispose())
|
|
|
await ctx.plugin(LlmRuntime)
|
|
|
await ctx.plugin(Messages, {
|
|
|
- protocol: 'messages',
|
|
|
baseURL: Messages.MESSAGES_BASE_URL,
|
|
|
maxTokens: 4096,
|
|
|
...models === undefined ? {} : { models },
|
|
|
@@ -60,20 +66,24 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('DeepSeek Messages real API', ()
|
|
|
await reply('PROMPT_CLEARED')
|
|
|
})
|
|
|
|
|
|
- it('uploads, lists, retrieves, and reuses a Files image across Messages requests', async () => {
|
|
|
+ it('uploads, lists, retrieves, reuses, and replaces a deleted Files image across Messages requests', async () => {
|
|
|
const ctx = await boot()
|
|
|
await ctx.plugin(LocalAttachments)
|
|
|
const fetchImpl = globalThis.fetch
|
|
|
const uploads: string[] = []
|
|
|
const bodies: string[] = []
|
|
|
const files = new DeepSeekFilesClient({ baseURL: Messages.MESSAGES_BASE_URL, protocol: 'messages', apiKey: process.env.DEEPSEEK_API_KEY as string, fetch: fetchImpl })
|
|
|
+ const ownedFiles = new Set<ReturnType<typeof Messages.DeepSeekFileId>>()
|
|
|
+ cleanups.push(async () => {
|
|
|
+ for (const id of ownedFiles) await files.delete(id)
|
|
|
+ })
|
|
|
vi.stubGlobal('fetch', async (input: string | URL | Request, init?: RequestInit) => {
|
|
|
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
|
|
|
const response = await fetchImpl(input, init)
|
|
|
if (url === `${Messages.MESSAGES_BASE_URL}/v1/files` && init?.method === 'POST' && response.ok) {
|
|
|
const file = await response.clone().json() as { id: string }
|
|
|
uploads.push(file.id)
|
|
|
- cleanups.push(() => files.delete(Messages.DeepSeekFileId(file.id)))
|
|
|
+ ownedFiles.add(Messages.DeepSeekFileId(file.id))
|
|
|
}
|
|
|
if (url === `${Messages.MESSAGES_BASE_URL}/v1/messages`) {
|
|
|
expect(new Headers(init?.headers).get('anthropic-beta')).toBe(MESSAGES_FILES_BETA)
|
|
|
@@ -110,6 +120,80 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('DeepSeek Messages real API', ()
|
|
|
page = await files.list({ after, limit: 1_000 })
|
|
|
}
|
|
|
expect(page.data).toContainEqual(retrieved)
|
|
|
+ await files.delete(fileId)
|
|
|
+ ownedFiles.delete(fileId)
|
|
|
+ const recovered = await assemble(ctx.llm.stream(request), request.model)
|
|
|
+ expect(recovered.assembler.finish.kind).toBe('stop')
|
|
|
+ expect(recovered.message.content.filter(block => block.type === 'text').map(block => block.text).join('').toLowerCase()).toContain('red')
|
|
|
+ expect(uploads).toHaveLength(2)
|
|
|
+ expect(uploads[1]).not.toBe(fileId)
|
|
|
+ expect(bodies).toHaveLength(4)
|
|
|
+ expect(bodies[2]).toContain(`"file_id":"${fileId}"`)
|
|
|
+ expect(bodies[3]).toContain(`"file_id":"${uploads[1]}"`)
|
|
|
+ expect(bodies[3]).not.toContain('"type":"base64"')
|
|
|
+ })
|
|
|
+
|
|
|
+ it.each([false, true])('submits Loader package inventory and records HTTP acceptance with session-log enabled=%s', async (enabled) => {
|
|
|
+ const ctx = await boot()
|
|
|
+ await ctx.plugin(Loader)
|
|
|
+ await ctx.plugin(AgentRegistry)
|
|
|
+ await ctx.plugin(SessionStore)
|
|
|
+ await ctx.plugin(DeepSeekLlmApiExtensionRegistry)
|
|
|
+ await ctx.plugin(SessionLogDeepSeek, enabled ? { enabled: true } : {})
|
|
|
+ ctx.baseUrl = import.meta.url
|
|
|
+ // Select the source module while Loader owns its active package entry.
|
|
|
+ ctx.loader.internal = {
|
|
|
+ version: 'v2',
|
|
|
+ async import(specifier: string) {
|
|
|
+ if (specifier !== '@deepseek-ai/dsh-plugin-package-inventory-deepseek') throw new Error(`unexpected Loader import: ${specifier}`)
|
|
|
+ return PluginPackageInventoryDeepSeek
|
|
|
+ },
|
|
|
+ } as unknown as NonNullable<typeof ctx.loader.internal>
|
|
|
+ await ctx.loader.create({ name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek' })
|
|
|
+ await ctx.loader.await()
|
|
|
+ const packageIdentity = JSON.parse(await readFile(new URL('../../../plugin-package-inventory-deepseek/package.json', import.meta.url), 'utf8')) as { name: string; version: string }
|
|
|
+ const session = ctx.sessions.create(SessionId(`real-messages-extensions-${randomUUID()}`))
|
|
|
+ session.append('turn/start', { turn: 1 })
|
|
|
+ const fetchImpl = globalThis.fetch
|
|
|
+ let afterSeq: number = -1
|
|
|
+ let throughSeq = 0
|
|
|
+ let requests = 0
|
|
|
+ vi.stubGlobal('fetch', async (input: string | URL | Request, init?: RequestInit) => {
|
|
|
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
|
|
|
+ if (url !== `${Messages.MESSAGES_BASE_URL}/v1/messages`) return fetchImpl(input, init)
|
|
|
+ if (typeof init?.body !== 'string') throw new Error('expected a JSON Messages request')
|
|
|
+ const body = JSON.parse(init.body) as Record<string, unknown>
|
|
|
+ expect(body).toMatchObject({ dsh_plugin_packages: {
|
|
|
+ version: 1, packages: [{ name: packageIdentity.name, version: packageIdentity.version }],
|
|
|
+ } })
|
|
|
+ if (enabled) {
|
|
|
+ expect(body).toMatchObject({ dsh_session_log: {
|
|
|
+ version: 1, sessionFormatVersion: session.header.version, session: { id: session.id },
|
|
|
+ afterSeq, throughSeq,
|
|
|
+ events: Array.from({ length: throughSeq - afterSeq }, (_, index) => ({ seq: afterSeq + index + 1 })),
|
|
|
+ } })
|
|
|
+ } else expect(body).not.toHaveProperty('dsh_session_log')
|
|
|
+ expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(afterSeq)
|
|
|
+ const response = await fetchImpl(input, init)
|
|
|
+ expect(response.ok).toBe(true)
|
|
|
+ expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(afterSeq)
|
|
|
+ requests += 1
|
|
|
+ return response
|
|
|
+ })
|
|
|
+ for (let run = 0; run < 2; run++) {
|
|
|
+ afterSeq = SessionLogDeepSeek.acceptedThrough(session)
|
|
|
+ throughSeq = Number(session.seq) - 1
|
|
|
+ const assembler = new BlockAssembler()
|
|
|
+ for await (const chunk of ctx.llm.stream(options({ sessionId: session.id, reasoningEffort: ReasoningEffortId('off'), messages: [user('Reply with exactly PONG.')] }))) {
|
|
|
+ expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(enabled ? throughSeq : -1)
|
|
|
+ assembler.push(chunk)
|
|
|
+ }
|
|
|
+ expect(assembler.finish.kind).toBe('stop')
|
|
|
+ expect(assembler.blocks().filter(block => block.type === 'text').map(block => block.text).join('')).toContain('PONG')
|
|
|
+ expect(requests).toBe(run + 1)
|
|
|
+ if (run === 0) session.append('step/start', { turn: 1, step: 1 })
|
|
|
+ }
|
|
|
+ expect(session.snapshotEvents().filter(event => event.type === 'session-log-deepseek/delivery-accepted')).toHaveLength(enabled ? 2 : 0)
|
|
|
})
|
|
|
|
|
|
it.each(['off', 'low', 'high', 'max'])('streams text with %s effort', async (effort) => {
|