Browse Source

Update image tests for split adapters and durable offload failures

creatixchu 4 days ago
parent
commit
27d52a4edb

+ 1 - 1
packages/compaction/compaction-image-offload/tests/image-offload.spec.ts

@@ -10,7 +10,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
 import BasicCompactionEngine from '@deepseek-ai/dsh-compaction-basic'
 import TokenMeter from '@deepseek-ai/dsh-token-meter'
 import { ImageVariantId } from '@deepseek-ai/dsh-attachment'
-import { serializeRequestWithImages } from '@deepseek-ai/dsh-llm-deepseek/src/serialize.ts'
+import { serializeRequestWithImages } from '@deepseek-ai/dsh-llm-deepseek/src/protocols/chat-completions/serialize.ts'
 import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
 import { createAssistantMessage, createToolResultMessage, createUserMessage, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, ToolCallId } from '@deepseek-ai/dsh-llm'
 import type { ContentBlock, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'

+ 3 - 3
packages/llm/llm-deepseek/tests/messages/adapter.spec.ts

@@ -173,10 +173,10 @@ describe('Cordis provider composition', () => {
     const model = 'deepseek-v4-flash-vision-exp'
     const price = () => ctx.llm.imageRequestPricing('deepseek-official', model)!
     const dummy = { attachmentId: AttachmentId(`sha256:${'a'.repeat(64)}`), width: 1, height: 1, bytes: 3, mediaType: 'image/png' as const }
-    expect(price().priceImages([dummy])[0]?.text).toBeDefined()
+    expect(price().priceImages([{ type: 'image', attachment: dummy }])[0]?.text).toBeDefined()
     await ctx.plugin(LocalAttachments, { dshHome: home })
     const attachment = await ctx.attachments.saveImage({ data: await readFile(new URL('fixtures/red.png', import.meta.url)), mediaType: 'image/png' })
-    expect(price().priceImages([attachment])[0]?.text).not.toContain('/mounted/image.png')
+    expect(price().priceImages([{ type: 'image', attachment }])[0]?.text).not.toContain('/mounted/image.png')
     class MappedFiles extends Service {
       constructor(context: Context) { super(context, 'fs') }
       processPathFromHostPath(_path: string) { return '/mounted/image.png' }
@@ -185,7 +185,7 @@ describe('Cordis provider composition', () => {
     const message = user()
     await chunks(ctx.llm.stream(options({ model, messages: [{ ...message, content: [...message.content, { type: 'image', attachment }] }] })))
     expect(JSON.stringify(http.requests[0]?.body)).toContain('/mounted/image.png')
-    expect(price().priceImages([attachment])[0]?.text).toContain('/mounted/image.png')
+    expect(price().priceImages([{ type: 'image', attachment }])[0]?.text).toContain('/mounted/image.png')
   })
 
   async function boot(...args: Parameters<typeof server>) {

+ 4 - 4
packages/llm/llm-deepseek/tests/messages/files.spec.ts

@@ -123,7 +123,7 @@ describe('Messages Files requests', () => {
     await chunks(h.adapter.stream(original))
     expect(body(fetchImpl.mock.calls[0]?.[1]).match(/"file_id"/gu)).toHaveLength(2)
     h.ensureUploaded.mockRejectedValueOnce(new Error('offline'))
-    await expect(chunks(h.adapter.stream(original))).rejects.toMatchObject({ code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 })
+    await expect(chunks(h.adapter.stream(original))).rejects.toMatchObject({ failure: { code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 } })
     expect(fetchImpl).toHaveBeenCalledTimes(1)
     h.ensureUploaded.mockRejectedValueOnce(new Error('offline'))
     await chunks(h.adapter.stream(options({ model, messages: [{ ...user(), content: [
@@ -138,7 +138,7 @@ describe('Messages Files requests', () => {
   it('reads and prices only the retained occurrences selected by the logged offload', async () => {
     vi.stubGlobal('fetch', async () => success())
     const h = harness({ maxRequestFilesBytes: 3, imageOffloadByteQuantum: 1, maxImagesPerRequest: 2, imageOffloadCountQuantum: 1 })
-    const images = [{ type: 'image' as const, attachment: ref, offloaded: true }, { type: 'image' as const, attachment: second }]
+    const images = [{ type: 'image' as const, attachment: ref, offloaded: true as const }, { type: 'image' as const, attachment: second }]
     await chunks(h.adapter.stream(options({ model, messages: [{ ...user(), content: images }] })))
     expect(h.readImageRequest).toHaveBeenCalledExactlyOnceWith(second, expect.anything(), expect.any(AbortSignal))
     expect(h.adapter.imageRequestPricing('deepseek-official', model).priceImages(images).map(image => image.visualTokens)).toEqual([0, expect.any(Number)])
@@ -153,7 +153,7 @@ describe('Messages Files requests', () => {
     const h = harness(config)
     const original = request([ref, ref])
     const saved = JSON.stringify(original.messages)
-    await expect(chunks(h.adapter.stream(original))).rejects.toMatchObject({ code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 })
+    await expect(chunks(h.adapter.stream(original))).rejects.toMatchObject({ failure: { code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 } })
     expect(h.readImageRequest).toHaveBeenCalledTimes(1)
     expect(h.ensureUploaded).not.toHaveBeenCalled()
     expect(fetchImpl).not.toHaveBeenCalled()
@@ -163,7 +163,7 @@ describe('Messages Files requests', () => {
   it('uses exact prepared bytes when the durable reference fits the Files budget', async () => {
     const h = harness({ maxRequestFilesBytes: 3, imageOffloadByteQuantum: 1, imageOffloadCountQuantum: 1 })
     h.readImageRequest.mockResolvedValue({ ...version(ref), bytes: 4, data: Uint8Array.of(1, 2, 3, 4) })
-    await expect(chunks(h.adapter.stream(request()))).rejects.toMatchObject({ code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 })
+    await expect(chunks(h.adapter.stream(request()))).rejects.toMatchObject({ failure: { code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 } })
     expect(h.ensureUploaded).not.toHaveBeenCalled()
   })
 

+ 15 - 8
packages/llm/llm-deepseek/tests/messages/serialize.spec.ts

@@ -1,7 +1,7 @@
 /** Request conversion and durable replay validation. */
 import { describe, expect, it, vi } from 'vitest'
 import { createAssistantMessage, createMessage, createSystemMessage, createToolResultMessage, ReasoningEffortId, ToolCallId } from '@deepseek-ai/dsh-llm'
-import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'
+import type { ContentBlock, GenerateOptions, ImageBlock, Message } from '@deepseek-ai/dsh-llm'
 import { AttachmentId, ImageVariantId } from '@deepseek-ai/dsh-attachment'
 import type { AttachmentStore, ImageAttachmentRef, RequestImageAttachment } from '@deepseek-ai/dsh-attachment'
 import { resolveAdapterOptions } from '../../src/config.ts'
@@ -248,7 +248,7 @@ describe('validated configuration', () => {
 
 describe('Messages images', () => {
   const ref: ImageAttachmentRef = { attachmentId: AttachmentId(`sha256:${'a'.repeat(64)}`), mediaType: 'image/png', width: 1, height: 1, bytes: 3 }
-  const image: ContentBlock = { type: 'image', attachment: ref }
+  const image: ImageBlock = { type: 'image', attachment: ref }
   const version: RequestImageAttachment = { attachment: ref, variantId: ImageVariantId(`sha256:${'b'.repeat(64)}`), mediaType: 'image/png', bytes: 3, data: Uint8Array.of(1, 2, 3), width: 1, height: 1, depth: 'uchar', space: 'srgb', hasAlpha: false }
   const access = () => ({ readonlyPath: '/workspace/image.png' })
   const model = 'deepseek-v4-flash-vision-exp'
@@ -264,23 +264,30 @@ describe('Messages images', () => {
       { type: 'text', text: expect.stringContaining('/workspace/image.png') as string }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'AQID' } },
       { type: 'text' }, { type: 'image' },
     ] })
-    expect(imagePricing(connection, model, access).priceImages([ref])[0]?.visualTokens).toBeGreaterThan(0)
-    expect(imagePricing(connection, MODEL, access).priceImages([ref])[0]?.visualTokens).toBe(0)
+    expect(imagePricing(connection, model, access).priceImages([image])[0]?.visualTokens).toBeGreaterThan(0)
+    expect(imagePricing(connection, MODEL, access).priceImages([image])[0]?.visualTokens).toBe(0)
   })
-  it('offloads an oldest prefix using exact encoded bytes and preserves durable references', async () => {
+  it('requires logged offload at exact encoded bytes and preserves durable references', async () => {
     const config = resolveAdapterOptions({
       maxInlineRequestImageBytes: 4, inlineImageOffloadByteQuantum: 1, maxImagesPerRequest: 2, imageOffloadCountQuantum: 1,
     })
     const history = [result('a', [image, image])]
     const prepared = await prepareImages(history, config, model, attachments, access, signal)
     expect(prepared.messages[0]?.content[0]).toMatchObject({ content: [image, image] })
-    expect(inlineImages(prepared.messages, prepared.versions, config, access)[0]?.content[0]).toMatchObject({ content: [{ type: 'text' }, { type: 'image' }] })
+    expect(() => inlineImages(prepared.messages, prepared.versions, config)).toThrow(expect.objectContaining({
+      failure: expect.objectContaining({ code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 }) as unknown,
+    }))
+    const offloaded: ImageBlock = { ...image, offloaded: true }
+    const retry = await prepareImages([result('a', [offloaded, image])], config, model, attachments, access, signal)
+    expect(inlineImages(retry.messages, retry.versions, config)[0]?.content[0]).toMatchObject({ content: [{ type: 'text' }, { type: 'image' }] })
     expect(history[0]?.content[0]).toMatchObject({ content: [image, image] })
-    expect(imagePricing(config, model, access).priceImages([ref, ref]).map(entry => entry.visualTokens))
+    expect(imagePricing(config, model, access).priceImages([image, image]).map(entry => entry.visualTokens))
       .toEqual([expect.any(Number), expect.any(Number)])
     const large = { readImageRequest: async () => ({ ...version, bytes: 30, data: new Uint8Array(30) }) } as unknown as AttachmentStore
     const exact = await prepareImages([result('a', [image])], config, model, large, access, signal)
-    expect(inlineImages(exact.messages, exact.versions, config, access)[0]?.content[0]).toMatchObject({ content: [{ type: 'text' }] })
+    expect(() => inlineImages(exact.messages, exact.versions, config)).toThrow(expect.objectContaining({
+      failure: expect.objectContaining({ code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 }) as unknown,
+    }))
   })
   it('rejects unsupported roles and unavailable image capabilities before HTTP', async () => {
     const history = [result('a', [image])]