Skip to content

Commit ef757c4

Browse files
committed
feat(core): flatten image config to experimental.image_model
1 parent d6b5311 commit ef757c4

7 files changed

Lines changed: 50 additions & 91 deletions

File tree

‎packages/client/src/promise/generated/types.ts‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/core/src/config/normalize.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -495,14 +495,14 @@ function normalizeExperimental(
495495
)
496496
if (value !== undefined) result.media_blob = value
497497
}
498-
if (own(experimental, "image_generation")) {
498+
if (own(experimental, "image_model")) {
499499
const value = decodeEncoded(
500-
ConfigExperimental.Info.fields.image_generation,
501-
experimental.image_generation,
502-
["experimental", "image_generation"],
500+
ConfigExperimental.Info.fields.image_model,
501+
experimental.image_model,
502+
["experimental", "image_model"],
503503
diagnostics,
504504
)
505-
if (value !== undefined) result.image_generation = value
505+
if (value !== undefined) result.image_model = value
506506
}
507507
if (own(experimental, "schedules")) {
508508
const value = decodeEncoded(

‎packages/core/src/tool/plugin/image-generate.ts‎

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,7 @@ export const Plugin = {
8484
const available = yield* models.available(["image"])
8585
if (!input) {
8686
// A configured default is a preference: when it can't serve this call, pick automatically and say so.
87-
const settings = settingsFrom(yield* config.entries())
88-
const configured = settings ? settings.model : undefined
87+
const configured = configuredModel(yield* config.entries())
8988
const preferred = configured
9089
? available.find((model) => model.providerID === configured.providerID && model.id === configured.model)
9190
: undefined
@@ -224,11 +223,9 @@ export const Plugin = {
224223
Effect.catchTag("Environment.NotFound", () => Effect.succeed(false)),
225224
)
226225

227-
// Registered only while an image model is available and config doesn't turn the tool off, re-evaluated
228-
// whenever either changes. Subscribing before the first read keeps a change in between from being missed.
229-
const wanted = Effect.all([config.entries(), models.available(["image"])]).pipe(
230-
Effect.map(([entries, available]) => settingsFrom(entries) !== false && available.length > 0),
231-
)
226+
// Registered only while an image model is available, re-evaluated whenever the catalog changes.
227+
// Subscribing before the first read keeps a change in between from being missed.
228+
const wanted = models.available(["image"]).pipe(Effect.map((available) => available.length > 0))
232229
let enabled = false
233230
const refresh = wanted.pipe(
234231
Effect.flatMap((next) => {
@@ -237,7 +234,7 @@ export const Plugin = {
237234
return ctx.tool.reload()
238235
}),
239236
)
240-
yield* ctx.event.subscribe({ types: ["model.updated", "config.updated"] }).pipe(
237+
yield* ctx.event.subscribe({ types: ["model.updated"] }).pipe(
241238
Stream.runForEach(() => refresh),
242239
Effect.forkScoped({ startImmediately: true }),
243240
)
@@ -379,10 +376,10 @@ export function defaultModel(
379376
}
380377

381378
// Read per field so a later config that sets other experimental options doesn't hide this one.
382-
function settingsFrom(entries: readonly Entry[]) {
379+
function configuredModel(entries: readonly Entry[]) {
383380
return entries.findLast(
384-
(entry): entry is Document => entry.type === "document" && entry.info.experimental?.image_generation !== undefined,
385-
)?.info.experimental?.image_generation
381+
(entry): entry is Document => entry.type === "document" && entry.info.experimental?.image_model !== undefined,
382+
)?.info.experimental?.image_model
386383
}
387384

388385
function acceptsImages(model: Model.Info) {

‎packages/core/test/config/normalization.test.ts‎

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,7 @@ describe("ConfigNormalize", () => {
545545
todo: true,
546546
background_compaction: true,
547547
media_blob: true,
548-
image_generation: { model: "openai/gpt-image-2" },
548+
image_model: "openai/gpt-image-2",
549549
subagent_depth: 0,
550550
location_idle_timeout: "2 hours",
551551
policies: [{ action: "provider.use", resource: "custom", effect: "allow" }],
@@ -557,7 +557,7 @@ describe("ConfigNormalize", () => {
557557
todo: true,
558558
background_compaction: true,
559559
media_blob: true,
560-
image_generation: { model: { providerID: "openai", model: "gpt-image-2" } },
560+
image_model: { providerID: "openai", model: "gpt-image-2" },
561561
subagent_depth: 0,
562562
location_idle_timeout: "7200000 millis",
563563
},
@@ -571,18 +571,15 @@ describe("ConfigNormalize", () => {
571571
})
572572
})
573573

574-
test("normalizes the image generation setting", () => {
575-
expect(normalized({ experimental: { image_generation: false } }).encoded.experimental).toEqual({
576-
image_generation: false,
574+
test("normalizes the image model setting", () => {
575+
expect(normalized({ experimental: { image_model: "openai/gpt-image-2#hd" } }).encoded.experimental).toEqual({
576+
image_model: { providerID: "openai", model: "gpt-image-2", variant: "hd" },
577577
})
578-
expect(normalized({ experimental: { image_generation: {} } }).encoded.experimental).toEqual({
579-
image_generation: {},
580-
})
581-
for (const value of [true, { model: "no-slash" }, { model: 1 }, "openai/gpt-image-2"]) {
582-
const result = normalized({ experimental: { image_generation: value } })
578+
for (const value of [false, true, "no-slash", 1, { model: "openai/gpt-image-2" }]) {
579+
const result = normalized({ experimental: { image_model: value } })
583580
expect(result.encoded.experimental).toBeUndefined()
584581
expect(result.diagnostics.map((item) => [item.kind, item.path])).toEqual([
585-
["invalid", ["experimental", "image_generation"]],
582+
["invalid", ["experimental", "image_model"]],
586583
])
587584
}
588585
})

‎packages/core/test/tool-image-generate.test.ts‎

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -128,21 +128,22 @@ const setup = (providerID = "test") =>
128128
return { session, run, notice, directory: location.directory }
129129
})
130130

131-
const configure = (image_generation: ConfigExperimental.Info["image_generation"]) =>
131+
const configure = (image_model: ConfigExperimental.Info["image_model"]) =>
132132
Effect.gen(function* () {
133133
const config = yield* Config.Test
134134
yield* config.setEntries([
135135
new Document({
136136
type: "document",
137-
info: new Info({ experimental: new ConfigExperimental.Info({ image_generation }) }),
137+
info: new Info({ experimental: new ConfigExperimental.Info({ image_model }) }),
138138
}),
139139
])
140140
const bus = yield* Bus.Service
141141
yield* bus.publish(Event.Updated, {})
142142
})
143143

144144
const configuredModel = (providerID: string, model: string) => ({
145-
model: { providerID: Provider.ID.make(providerID), model: Model.ID.make(model) },
145+
providerID: Provider.ID.make(providerID),
146+
model: Model.ID.make(model),
146147
})
147148

148149
const registered = Tool.Service.use((registry) => toolDefinitions(registry)).pipe(
@@ -172,19 +173,6 @@ describe("image_generate", () => {
172173
}),
173174
)
174175

175-
it.live("is not registered while config turns it off", () =>
176-
Effect.gen(function* () {
177-
yield* imageModel("test", "painter")
178-
yield* configure(false)
179-
yield* setup()
180-
expect(yield* registered).toBe(false)
181-
yield* configure({})
182-
expect(yield* until(true)).toBe(true)
183-
yield* configure(false)
184-
expect(yield* until(false)).toBe(false)
185-
}),
186-
)
187-
188176
it.live("uses the configured model unless the agent picks one, and falls back with a note", () =>
189177
Effect.gen(function* () {
190178
yield* imageModel("test", "painter", (model) => {
@@ -386,23 +374,19 @@ describe("image_generate", () => {
386374
)
387375

388376
yield* configure({
389-
model: {
390-
providerID: Provider.ID.make("test"),
391-
model: Model.ID.make("painter"),
392-
variant: Model.VariantID.make("hd"),
393-
},
377+
providerID: Provider.ID.make("test"),
378+
model: Model.ID.make("painter"),
379+
variant: Model.VariantID.make("hd"),
394380
})
395381
const configured = yield* run({ prompt: "x" })
396382
expect(configured.output).toMatchObject({ model: "test/painter#hd" })
397383
yield* notice(configured)
398384
expect(fixture.requests.at(-1)?.model.variant).toBe(Model.VariantID.make("hd"))
399385

400386
yield* configure({
401-
model: {
402-
providerID: Provider.ID.make("test"),
403-
model: Model.ID.make("painter"),
404-
variant: Model.VariantID.make("bogus"),
405-
},
387+
providerID: Provider.ID.make("test"),
388+
model: Model.ID.make("painter"),
389+
variant: Model.VariantID.make("bogus"),
406390
})
407391
const fallback = yield* run({ prompt: "x" })
408392
expect(text(fallback)).toContain('test/painter#bogus has no variant "bogus"; used ')

‎packages/protocol/openapi.json‎

Lines changed: 14 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/schema/src/config/experimental.ts‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,10 @@ export class Info extends Schema.Class<Info>("ConfigExperimental.Info")({
2020
description:
2121
"Store new session media once in a deduplicated blob table instead of inline in every message row. Defaults to false.",
2222
}),
23-
image_generation: Schema.Union([
24-
Schema.Literal(false),
25-
Schema.Struct({ model: ConfigModel.Selection.pipe(optional) }),
26-
])
27-
.pipe(optional)
28-
.annotate({
29-
description:
30-
"Image generation tool settings. `model` sets the default image model (providerID/modelID) used when the agent doesn't choose one; set to false to disable the image_generate tool.",
31-
}),
23+
image_model: ConfigModel.Selection.pipe(optional).annotate({
24+
description:
25+
"Default model for the image_generate tool when the agent doesn't choose one, as providerID/modelID. Falls back to automatic selection when it isn't available.",
26+
}),
3227
schedules: Schema.Boolean.pipe(optional).annotate({
3328
description: "Enable experimental scheduled tasks that wake a session with a prompt. Defaults to false.",
3429
}),

0 commit comments

Comments
 (0)