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.
71 lines
2.5 KiB
TypeScript
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)
|
|
})
|
|
})
|