Skip to content

Commit e2d1036

Browse files
committed
feat(core): support model variants in image_generate
1 parent 0fd19c6 commit e2d1036

2 files changed

Lines changed: 83 additions & 8 deletions

File tree

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

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const Input = Schema.Struct({
5050
}),
5151
model: Schema.optionalKey(Schema.String).annotate({
5252
description:
53-
'NEVER set this unless the user asks for a particular image model. Written as "providerID/modelID". Do not guess the ID: look it up with the models tool using type: "image".',
53+
'NEVER set this unless the user explicitly asks for a particular image model or variant. The value is written as "providerID/modelID", or "providerID/modelID#variant" to include a variant. Do not guess the ID: look it up with the models tool using type: "image".',
5454
}),
5555
path: Schema.optionalKey(Schema.String).annotate({
5656
description:
@@ -91,7 +91,12 @@ export const Plugin = {
9191
const preferred = configured
9292
? available.find((model) => model.providerID === configured.providerID && model.id === configured.model)
9393
: undefined
94-
if (preferred && (!images || acceptsImages(preferred))) return { model: preferred }
94+
const variantMissing =
95+
configured?.variant !== undefined &&
96+
preferred !== undefined &&
97+
!preferred.variants.some((variant) => variant.id === configured.variant)
98+
if (preferred && !variantMissing && (!images || acceptsImages(preferred)))
99+
return { model: preferred, variant: configured?.variant }
95100
const providerID = (yield* ctx.session.get({ sessionID })).model?.providerID
96101
const model = defaultModel(available, { providerID, images })
97102
if (!model)
@@ -101,19 +106,33 @@ export const Plugin = {
101106
: "No image model is available.",
102107
})
103108
if (!configured) return { model }
104-
const ref = `${configured.providerID}/${configured.model}`
105-
const reason = preferred ? "doesn't accept input images" : "isn't available"
109+
const ref = `${configured.providerID}/${configured.model}${configured.variant ? `#${configured.variant}` : ""}`
110+
const reason = !preferred
111+
? "isn't available"
112+
: variantMissing
113+
? `has no variant "${configured.variant}"`
114+
: "doesn't accept input images"
106115
return { model, note: ` ${ref} ${reason}; used ${model.providerID}/${model.id}.` }
107116
}
108117
const ref = yield* Effect.try({
109118
try: () => Model.Ref.parse(input),
110-
catch: () => new ToolFailure({ message: `Invalid model "${input}". Use "providerID/modelID".` }),
119+
catch: () =>
120+
new ToolFailure({
121+
message: `Invalid model "${input}". Use "providerID/modelID" or "providerID/modelID#variant".`,
122+
}),
111123
})
112124
const model = available.find((model) => model.providerID === ref.providerID && model.id === ref.id)
113125
if (model === undefined)
114126
return yield* new ToolFailure({
115127
message: `Model "${ref.providerID}/${ref.id}" is not an available image model. Use the models tool with type: "image" to see what is available.`,
116128
})
129+
if (ref.variant !== undefined && !model.variants.some((variant) => variant.id === ref.variant))
130+
return yield* new ToolFailure({
131+
message:
132+
model.variants.length === 0
133+
? `Model "${ref.providerID}/${ref.id}" has no variants. Omit the variant.`
134+
: `Variant "${ref.variant}" is not available for "${ref.providerID}/${ref.id}". Available: ${model.variants.map((variant) => variant.id).join(", ")}.`,
135+
})
117136
if (images && !acceptsImages(model)) {
118137
const capable = available.filter(acceptsImages).slice(0, 3)
119138
return yield* new ToolFailure({
@@ -123,7 +142,7 @@ export const Plugin = {
123142
: `Model "${ref.providerID}/${ref.id}" does not accept input images. Image models that do: ${capable.map((model) => `${model.providerID}/${model.id}`).join(", ")}.`,
124143
})
125144
}
126-
return { model }
145+
return { model, variant: ref.variant }
127146
})
128147

129148
const readImage = Effect.fn("ImageGenerateTool.readImage")(function* (
@@ -236,7 +255,8 @@ export const Plugin = {
236255
const source = { type: "tool" as const, messageID: context.messageID, id: context.id }
237256
const resolved = yield* resolveModel(input.model, context.sessionID, input.images !== undefined)
238257
const model = resolved.model
239-
const ref = `${model.providerID}/${model.id}`
258+
const variant = resolved.variant
259+
const ref = `${model.providerID}/${model.id}${variant ? `#${variant}` : ""}`
240260
const target = input.path ? yield* access.resolve({ path: input.path, kind: "file" }) : undefined
241261
const requested =
242262
target?.absolute ??
@@ -272,7 +292,11 @@ export const Plugin = {
272292
const run = Effect.gen(function* () {
273293
const result = yield* generate
274294
.image({
275-
model: Model.Ref.make({ providerID: model.providerID, id: model.id }),
295+
model: Model.Ref.make({
296+
providerID: model.providerID,
297+
id: model.id,
298+
...(variant ? { variant } : {}),
299+
}),
276300
prompt: input.prompt,
277301
n: 1,
278302
...(images ? { images } : {}),

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,57 @@ describe("image_generate", () => {
355355
}),
356356
)
357357

358+
it.live("passes a model variant through, from the input or from config", () =>
359+
Effect.gen(function* () {
360+
yield* imageModel("test", "painter", (model) => {
361+
model.variants = [{ id: Model.VariantID.make("hd"), settings: { quality: "high" } }]
362+
})
363+
yield* imageModel("test", "plain")
364+
const { run, notice } = yield* setup()
365+
366+
const explicit = yield* run({ prompt: "x", model: "test/painter#hd" })
367+
expect(explicit.output).toMatchObject({ model: "test/painter#hd" })
368+
yield* notice(explicit)
369+
expect(fixture.requests.at(-1)?.model).toEqual(
370+
Model.Ref.make({
371+
providerID: Provider.ID.make("test"),
372+
id: Model.ID.make("painter"),
373+
variant: Model.VariantID.make("hd"),
374+
}),
375+
)
376+
expect(failure(yield* run({ prompt: "x", model: "test/painter#bogus" }))).toBe(
377+
'Variant "bogus" is not available for "test/painter". Available: hd.',
378+
)
379+
expect(failure(yield* run({ prompt: "x", model: "test/plain#hd" }))).toBe(
380+
'Model "test/plain" has no variants. Omit the variant.',
381+
)
382+
383+
yield* configure({
384+
model: {
385+
providerID: Provider.ID.make("test"),
386+
model: Model.ID.make("painter"),
387+
variant: Model.VariantID.make("hd"),
388+
},
389+
})
390+
const configured = yield* run({ prompt: "x" })
391+
expect(configured.output).toMatchObject({ model: "test/painter#hd" })
392+
yield* notice(configured)
393+
expect(fixture.requests.at(-1)?.model.variant).toBe(Model.VariantID.make("hd"))
394+
395+
yield* configure({
396+
model: {
397+
providerID: Provider.ID.make("test"),
398+
model: Model.ID.make("painter"),
399+
variant: Model.VariantID.make("bogus"),
400+
},
401+
})
402+
const fallback = yield* run({ prompt: "x" })
403+
expect(text(fallback)).toContain('test/painter#bogus has no variant "bogus"; used ')
404+
yield* notice(fallback)
405+
expect(fixture.requests.at(-1)?.model.variant).toBeUndefined()
406+
}),
407+
)
408+
358409
it.live("rejects input files that are not images", () =>
359410
Effect.gen(function* () {
360411
yield* imageModel("test", "painter")

0 commit comments

Comments
 (0)