11import type OpenAI from 'openai'
2+ import type { ReferenceImage , VisionImageDetail } from '../../types'
23import type { StudioAssistantMessage , StudioMessage , StudioToolPart } from '../domain/types'
34import {
45 type StudioStoredAssistantPayload ,
56 type StudioStoredAssistantToolCall ,
67} from './studio-provider-message'
78
9+ const REFERENCE_IMAGES_START = '[STUDIO_REFERENCE_IMAGES]'
10+ const REFERENCE_IMAGES_END = '[/STUDIO_REFERENCE_IMAGES]'
11+
812export function buildStudioConversationMessages ( input : {
913 messages : StudioMessage [ ]
1014} ) : OpenAI . Chat . Completions . ChatCompletionMessageParam [ ] {
@@ -13,7 +17,7 @@ export function buildStudioConversationMessages(input: {
1317
1418function toConversationMessages ( message : StudioMessage ) : OpenAI . Chat . Completions . ChatCompletionMessageParam [ ] {
1519 if ( message . role === 'user' ) {
16- return [ { role : 'user' , content : message . text } ]
20+ return [ { role : 'user' , content : buildUserMessageContent ( message . text ) } ]
1721 }
1822
1923 if ( message . role !== 'assistant' ) {
@@ -126,7 +130,81 @@ function flattenAssistantMessage(message: Extract<StudioMessage, { role: 'assist
126130 }
127131
128132 return sections . join ( '\n\n' ) . trim ( )
129- }
133+ }
134+
135+ function buildUserMessageContent (
136+ inputText : string ,
137+ ) : string | Array < { type : 'text' ; text : string } | { type : 'image_url' ; image_url : { url : string ; detail : VisionImageDetail } } > {
138+ const parsed = parseReferenceImagesFromInput ( inputText )
139+ if ( parsed . referenceImages . length === 0 ) {
140+ return inputText
141+ }
142+
143+ return [
144+ {
145+ type : 'text' ,
146+ text : parsed . text || 'Use the provided reference images as context.' ,
147+ } ,
148+ ...parsed . referenceImages . map ( ( image ) => ( {
149+ type : 'image_url' as const ,
150+ image_url : {
151+ url : image . url ,
152+ detail : image . detail || 'auto' ,
153+ } ,
154+ } ) ) ,
155+ ]
156+ }
157+
158+ function parseReferenceImagesFromInput ( inputText : string ) : {
159+ text : string
160+ referenceImages : ReferenceImage [ ]
161+ } {
162+ const startIndex = inputText . indexOf ( REFERENCE_IMAGES_START )
163+ const endIndex = inputText . indexOf ( REFERENCE_IMAGES_END )
164+ if ( startIndex < 0 || endIndex < startIndex ) {
165+ return {
166+ text : inputText ,
167+ referenceImages : [ ] ,
168+ }
169+ }
170+
171+ const before = inputText . slice ( 0 , startIndex ) . trimEnd ( )
172+ const after = inputText . slice ( endIndex + REFERENCE_IMAGES_END . length ) . trimStart ( )
173+ const block = inputText . slice ( startIndex + REFERENCE_IMAGES_START . length , endIndex )
174+
175+ return {
176+ text : [ before , after ] . filter ( Boolean ) . join ( '\n\n' ) ,
177+ referenceImages : extractReferenceImages ( block ) ,
178+ }
179+ }
180+
181+ function extractReferenceImages ( block : string ) : ReferenceImage [ ] {
182+ return block
183+ . split ( / \r ? \n / )
184+ . map ( ( line ) => line . trim ( ) )
185+ . filter ( ( line ) => line . startsWith ( '-' ) )
186+ . map ( parseReferenceImageLine )
187+ . filter ( ( image ) : image is ReferenceImage => Boolean ( image ) )
188+ }
189+
190+ function parseReferenceImageLine ( line : string ) : ReferenceImage | null {
191+ const match = line . match ( / ^ - + \s * [ ^ : ] + : \s * ( h t t p s ? : \/ \/ \S + ?) (?: \s + \( d e t a i l : \s * ( a u t o | l o w | h i g h ) \) ) ? \s * $ / i)
192+ if ( ! match ) {
193+ return null
194+ }
195+
196+ return {
197+ url : match [ 1 ] ,
198+ detail : normalizeDetail ( match [ 2 ] ) ,
199+ }
200+ }
201+
202+ function normalizeDetail ( value : string | undefined ) : VisionImageDetail {
203+ if ( value === 'low' || value === 'high' || value === 'auto' ) {
204+ return value
205+ }
206+ return 'auto'
207+ }
130208
131209
132210
0 commit comments