@@ -3,11 +3,13 @@ export * as ImageGenerateTool from "./image-generate.js"
33import path from "path"
44import { ToolFailure } from "@opencode/ai/effect"
55import type { Context } from "@opencode/plugin/effect/plugin"
6+ import type { Document , Entry } from "@opencode/schema/config"
67import { Identifier } from "@opencode/schema/identifier"
78import type { Session } from "@opencode/schema/session"
89import { Global } from "@opencode/util/global"
910import { Duration , Effect , Schema , Stream } from "effect"
1011import { HttpClient , HttpClientRequest , HttpClientResponse } from "effect/http"
12+ import { Config } from "../../config.js"
1113import { Environment } from "../../environment/index.js"
1214import { FileAccess } from "../../file-access.js"
1315import { Generate } from "../../generate.js"
@@ -67,6 +69,7 @@ export const Plugin = {
6769 const generate = yield * Generate . Service
6870 const global = yield * Global . Service
6971 const http = yield * HttpClient . HttpClient
72+ const config = yield * Config . Service
7073 const models = yield * Model . Service
7174 const permission = yield * Permission . Service
7275
@@ -77,12 +80,25 @@ export const Plugin = {
7780 ) {
7881 const available = yield * models . available ( [ "image" ] )
7982 if ( ! input ) {
83+ // A configured default is a preference: when it can't serve this call, pick automatically and say so.
84+ const settings = settingsFrom ( yield * config . entries ( ) )
85+ const configured = settings ? settings . model : undefined
86+ const preferred = configured
87+ ? available . find ( ( model ) => model . providerID === configured . providerID && model . id === configured . model )
88+ : undefined
89+ if ( preferred && ( ! images || acceptsImages ( preferred ) ) ) return { model : preferred }
8090 const providerID = ( yield * ctx . session . get ( { sessionID } ) ) . model ?. providerID
8191 const model = defaultModel ( available , { providerID, images } )
82- if ( model ) return model
83- return yield * new ToolFailure ( {
84- message : images ? "None of the available image models accept input images." : "No image model is available." ,
85- } )
92+ if ( ! model )
93+ return yield * new ToolFailure ( {
94+ message : images
95+ ? "None of the available image models accept input images."
96+ : "No image model is available." ,
97+ } )
98+ if ( ! configured ) return { model }
99+ const ref = `${ configured . providerID } /${ configured . model } `
100+ const reason = preferred ? "doesn't accept input images" : "isn't available"
101+ return { model, note : ` ${ ref } ${ reason } ; used ${ model . providerID } /${ model . id } .` }
86102 }
87103 const ref = yield * Effect . try ( {
88104 try : ( ) => Model . Ref . parse ( input ) ,
@@ -102,7 +118,7 @@ export const Plugin = {
102118 : `Model "${ ref . providerID } /${ ref . id } " does not accept input images. Image models that do: ${ capable . map ( ( model ) => `${ model . providerID } /${ model . id } ` ) . join ( ", " ) } .` ,
103119 } )
104120 }
105- return model
121+ return { model }
106122 } )
107123
108124 const readImage = Effect . fn ( "ImageGenerateTool.readImage" ) ( function * (
@@ -161,21 +177,24 @@ export const Plugin = {
161177 Effect . catchTag ( "Environment.NotFound" , ( ) => Effect . succeed ( false ) ) ,
162178 )
163179
164- // Registered only while an image model is available, and re-evaluated whenever the model catalog changes.
165- // Subscribing before the first read keeps a change in between from being missed.
180+ // Registered only while an image model is available and config doesn't turn the tool off, re-evaluated
181+ // whenever either changes. Subscribing before the first read keeps a change in between from being missed.
182+ const wanted = Effect . all ( [ config . entries ( ) , models . available ( [ "image" ] ) ] ) . pipe (
183+ Effect . map ( ( [ entries , available ] ) => settingsFrom ( entries ) !== false && available . length > 0 ) ,
184+ )
166185 let enabled = false
167- const refresh = models . available ( [ "image" ] ) . pipe (
168- Effect . flatMap ( ( available ) => {
169- if ( available . length > 0 === enabled ) return Effect . void
170- enabled = available . length > 0
186+ const refresh = wanted . pipe (
187+ Effect . flatMap ( ( next ) => {
188+ if ( next === enabled ) return Effect . void
189+ enabled = next
171190 return ctx . tool . reload ( )
172191 } ) ,
173192 )
174- yield * ctx . event . subscribe ( { types : [ "model.updated" ] } ) . pipe (
193+ yield * ctx . event . subscribe ( { types : [ "model.updated" , "config.updated" ] } ) . pipe (
175194 Stream . runForEach ( ( ) => refresh ) ,
176195 Effect . forkScoped ( { startImmediately : true } ) ,
177196 )
178- enabled = ( yield * models . available ( [ "image" ] ) ) . length > 0
197+ enabled = yield * wanted
179198
180199 yield * ctx . tool
181200 . transform ( ( editor ) => {
@@ -189,7 +208,8 @@ export const Plugin = {
189208 execute : ( input , context ) =>
190209 Effect . gen ( function * ( ) {
191210 const source = { type : "tool" as const , messageID : context . messageID , id : context . id }
192- const model = yield * resolveModel ( input . model , context . sessionID , input . images !== undefined )
211+ const resolved = yield * resolveModel ( input . model , context . sessionID , input . images !== undefined )
212+ const model = resolved . model
193213 const ref = `${ model . providerID } /${ model . id } `
194214 const target = input . path ? yield * access . resolve ( { path : input . path , kind : "file" } ) : undefined
195215 const requested =
@@ -250,7 +270,9 @@ export const Plugin = {
250270 if ( yield * exists ( file ) )
251271 return yield * new ToolFailure ( { message : `${ file } already exists. Choose a new path.` } )
252272 yield * environment . files . write ( file , bytes )
253- const note = mime === "image/png" ? "" : ` The model returned ${ mime } , so the file is in that format.`
273+ const note =
274+ ( resolved . note ?? "" ) +
275+ ( mime === "image/png" ? "" : ` The model returned ${ mime } , so the file is in that format.` )
254276 return {
255277 output : { path : file , model : ref } ,
256278 content : [
@@ -284,6 +306,13 @@ export function defaultModel(
284306 return PREFERRED_FAMILIES . flatMap ( ( family ) => pool . find ( ( model ) => model . family === family ) ?? [ ] ) [ 0 ] ?? pool [ 0 ]
285307}
286308
309+ // Read per field so a later config that sets other experimental options doesn't hide this one.
310+ function settingsFrom ( entries : readonly Entry [ ] ) {
311+ return entries . findLast (
312+ ( entry ) : entry is Document => entry . type === "document" && entry . info . experimental ?. image_generation !== undefined ,
313+ ) ?. info . experimental ?. image_generation
314+ }
315+
287316function acceptsImages ( model : Model . Info ) {
288317 return model . capabilities . input . some ( ( item ) => item . startsWith ( "image" ) )
289318}
0 commit comments