Update image tests for split adapters and durable offload failures
This commit is contained in:
1 parent
a7070008e7
commit
27d52a4edb
4 files changed
+23
-16
No files matched your search
@@ -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'
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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])]
|
||||
|
||||
Reference in new issue
Block a user