@@ -9,6 +9,7 @@ type CapturedOpenRouterRequest = {
99 xTitle : string | undefined
1010 httpReferer : string | undefined
1111 userAgent : string | undefined
12+ userMessageContent ?: unknown
1213}
1314
1415function getRequestUrl ( input : RequestInfo | URL ) : string {
@@ -40,10 +41,22 @@ function installOpenRouterRequestCapture(capture: CapturedOpenRouterRequest[], b
4041 if ( new URL ( url ) . origin === targetOrigin ) {
4142 const xTitle = getHeaderValue ( init , "X-Title" ) ?? getHeaderValue ( init , "x-title" )
4243 if ( xTitle !== undefined ) {
44+ let userMessageContent : unknown
45+ if ( init ?. body && typeof init . body === "string" ) {
46+ try {
47+ const body = JSON . parse ( init . body )
48+ const messages : Array < { role ?: string ; content ?: unknown } > = body . messages ?? [ ]
49+ const lastUser = [ ...messages ] . reverse ( ) . find ( ( m ) => m . role === "user" )
50+ userMessageContent = lastUser ?. content
51+ } catch {
52+ // ignore parse errors
53+ }
54+ }
4355 capture . push ( {
4456 xTitle,
4557 httpReferer : getHeaderValue ( init , "HTTP-Referer" ) ?? getHeaderValue ( init , "http-referer" ) ,
4658 userAgent : getHeaderValue ( init , "User-Agent" ) ?? getHeaderValue ( init , "user-agent" ) ,
59+ userMessageContent,
4760 } )
4861 }
4962 }
@@ -82,7 +95,7 @@ suite("OpenRouter provider", function () {
8295 await globalThis . api . setConfiguration ( {
8396 apiProvider : "openrouter" as const ,
8497 openRouterApiKey : aimockUrl && ! isRecord ? "mock-key" : OPENROUTER_API_KEY ! ,
85- openRouterModelId : "openai/gpt-4.1 " ,
98+ openRouterModelId : "anthropic/claude-haiku-4-5 " ,
8699 ...( aimockUrl && { openRouterBaseUrl : `${ aimockUrl } /v1` } ) ,
87100 } )
88101 } )
@@ -92,6 +105,41 @@ suite("OpenRouter provider", function () {
92105 restoreFetch = undefined
93106 } )
94107
108+ test ( "Should forward base64 images as image_url content parts in outbound request" , async ( ) => {
109+ requests . length = 0
110+
111+ // 8x8 red PNG (1x1 is too small and rejected by Anthropic's vision API)
112+ const base64Png =
113+ "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAEklEQVR4nGP4z8CAFWEXHbQSACj/P8Fu7N9hAAAAAElFTkSuQmCC"
114+ const dataUri = `data:image/png;base64,${ base64Png } `
115+
116+ const api = globalThis . api
117+ const taskId = await api . startNewTask ( {
118+ configuration : { mode : "ask" , autoApprovalEnabled : true } ,
119+ text : "openrouter-image-e2e: describe the image in one word." ,
120+ images : [ dataUri ] ,
121+ } )
122+
123+ await waitUntilCompleted ( { api, taskId } )
124+
125+ // Find the first request that contains the probe tag
126+ const probeRequest = requests . find ( ( r ) => {
127+ const content = r . userMessageContent
128+ return Array . isArray ( content ) && JSON . stringify ( content ) . includes ( "openrouter-image-e2e" )
129+ } )
130+
131+ assert . ok ( probeRequest , "Should have captured an outbound request containing the probe tag" )
132+
133+ const content = probeRequest . userMessageContent as Array < { type : string ; image_url ?: { url : string } } >
134+ const imagePart = content . find ( ( p ) => p . type === "image_url" )
135+ assert . ok ( imagePart , "User message should contain an image_url content part" )
136+ assert . strictEqual (
137+ imagePart ?. image_url ?. url ,
138+ dataUri ,
139+ "image_url.url should be the original data URI passed via startNewTask" ,
140+ )
141+ } )
142+
95143 test ( "Should identify as Zoo Code in outbound DEFAULT_HEADERS" , async ( ) => {
96144 requests . length = 0
97145
0 commit comments