openpencil/tests/engine/app/ai/vision.test.ts
Danila Poyarkov 0ff6b414e3
feat(ai): choose a thinking level per message (#809)
Reasoning effort was a free-text profile field that only reached OpenAI
and OpenRouter, so Anthropic, Google, and DeepSeek models never thought
in direct chat. AI SDK 7 standardizes a `reasoning` call option that
those providers map to their own thinking settings, so profiles now
store one typed thinking level, shared with Pi, and requests pass it
through that option. OpenRouter's provider ignores the standard option
and receives its own reasoning option instead.

The composer offers the level next to the Design profile and reads it
per request, so a change applies to the next message without
rebuilding the transport. Saved profiles migrate from the Pi level or
the old effort string. Finished reasoning shows how long the model
thought while the block streamed.
2026-10-03 13:30:05 +04:00

71 lines
2.5 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import type { LanguageModel } from 'ai'
import { SceneGraph } from '@open-pencil/scene-graph'
import {
boundedImageScale,
inspectRenderedDesign,
type VisualInspectionDependencies
} from '@/app/ai/tools/vision'
import type { EditorStore } from '@/app/editor/session/create'
describe('isolated visual inspection', () => {
test('bounds renders and forwards only textual findings to the caller', async () => {
const graph = new SceneGraph()
const page = graph.getPages()[0]
const frame = graph.createNode('FRAME', page.id, { width: 2560, height: 1600 })
const rendered: Array<{ ids: string[]; scale: number }> = []
const inspections: unknown[] = []
const store = {
graph,
state: { currentPageId: page.id, selectedIds: new Set([frame.id]) },
renderExportImage: async (ids: string[], scale: number) => {
rendered.push({ ids, scale })
return new Uint8Array([1, 2, 3])
}
} as EditorStore
const dependencies: VisualInspectionDependencies = {
createRuntime: async () =>
({
kind: 'direct',
model: {} as LanguageModel,
role: {
requestedRole: 'vision',
profile: { maxOutputTokens: 8000, thinkingLevel: 'low' },
connection: { providerID: 'openrouter' }
}
}) as never,
inspect: async (options) => {
inspections.push(options)
return { text: 'Align the button with the form edge.' } as never
}
}
const result = await inspectRenderedDesign(store, {}, dependencies)
expect(rendered).toEqual([{ ids: [frame.id], scale: 0.5 }])
expect(result).toEqual({
analysis: 'Align the button with the form edge.',
inspectedNodeIds: [frame.id],
image: { width: 1280, height: 800 }
})
const request = inspections[0] as {
maxOutputTokens: number
providerOptions?: unknown
messages: Array<{ content: Array<{ type: string; data?: Uint8Array }> }>
}
expect(request.maxOutputTokens).toBe(1200)
expect(request.providerOptions).toEqual({ openrouter: { reasoning: { effort: 'low' } } })
expect(request.messages[0]?.content[1]?.type).toBe('file')
expect(request.messages[0]?.content[1]?.data).toEqual(new Uint8Array([1, 2, 3]))
expect(result).not.toHaveProperty('base64')
})
test('never upscales bounded images', () => {
expect(boundedImageScale(640, 400)).toBe(1)
expect(boundedImageScale(2560, 1600)).toBe(0.5)
})
})