feat(ai): add GPT Image 2.6 and default to Flare

This commit is contained in:
Filip Kujundžić committed 2026-11-05 11:13:43 +01:00
1 parent 8b64cc5076
commit be9cd9eb89
7 files changed
+156 -20

No files matched your search

@@ -463,10 +463,26 @@ describe('ImageGenerationDriver.generate provider routing', () => {
driver.generate({ prompt: 'a landscape' }),
);
expect(openaiImagesGenerateMock).toHaveBeenCalledWith(
expect.objectContaining({ model: 'gpt-image-2' }),
expect.objectContaining({ model: 'gpt-image-2.6-flare' }),
);
});
it.each(['gpt-image-2.6-flare', 'gpt-image-2.6-sunburst'])(
'routes %s and its OpenAI alias', async (model) => {
for (const id of [model, `openai/${model}`]) {
openaiImagesGenerateMock.mockResolvedValueOnce({
data: [{ url: 'https://oai/image.png' }],
});
await withDriverName('ai-image', () => driver.generate({
model: id, prompt: 'a landscape',
}));
expect(openaiImagesGenerateMock).toHaveBeenLastCalledWith(
expect.objectContaining({ model }),
);
}
},
);
it('rejects an excluded model regardless of caller casing', async () => {
await expect(withActor(() =>
driver.generate({
@@ -150,9 +150,9 @@ describe('OpenAiImageProvider construction', () => {
// ── Model catalog ───────────────────────────────────────────────────
describe('OpenAiImageProvider model catalog', () => {
it('returns gpt-image-2 as the default', () => {
it('returns gpt-image-2.6-flare as the default', () => {
const provider = makeProvider();
expect(provider.getDefaultModel()).toBe('gpt-image-2');
expect(provider.getDefaultModel()).toBe('gpt-image-2.6-flare');
});
it('drops the deprecated gpt-image-1 family and chatgpt-image-latest', () => {
@@ -907,3 +907,64 @@ it('keeps a landscape aspect hint landscape at gpt-image-2 sizes', async () => {
expect(w).toBeGreaterThan(h);
expect(Math.abs(w / h - 1.7)).toBeLessThan(0.1);
});
describe('OpenAiImageProvider GPT Image 2.6', () => {
it.each(['gpt-image-2.6-flare', 'gpt-image-2.6-sunburst'])(
'%s generates and edits with token-based billing', async (model) => {
const provider = makeProvider();
const response = {
data: [{ b64_json: 'AAAA' }],
usage: {
input_tokens: 100,
output_tokens: 800,
input_tokens_details: { text_tokens: 100, image_tokens: 0 },
},
};
generateMock.mockResolvedValue(response);
editMock.mockResolvedValue(response);
for (const input_image of [undefined, 'data:image/png;base64,AAAA']) {
const result = await withTestActor(() => provider.generate({
model, prompt: 'hi', quality: 'xhigh',
ratio: { w: 1024, h: 1024 }, input_image,
}));
expect(result).toBe('data:image/png;base64,AAAA');
}
for (const mock of [generateMock, editMock]) {
expect(mock.mock.calls[0]![0]).toMatchObject({
model, quality: 'xhigh', size: '1024x1024',
});
}
const [, entries] = batchIncrementUsagesSpy.mock.calls[0]!;
expect(entries).toEqual(expect.arrayContaining([
expect.objectContaining({
usageType: `openai:${model}:xhigh:1024x1024:output`,
costOverride: 2_400_000,
}),
]));
},
);
it.each([
['low', 1024, 1024, 823],
['medium', 1024, 1024, 1449],
['high', 1024, 1024, 1756],
['xhigh', 1024, 1024, 3122],
['max', 1024, 1024, 7024],
['auto', 1024, 1024, 1449],
['low', 2048, 1088, 571],
])('estimates %s at %sx%s from the 2.6 calculator', async (quality, w, h, tokens) => {
generateMock.mockResolvedValue({ data: [{ b64_json: 'AAAA' }] });
await withTestActor(() => makeProvider().generate({
model: 'gpt-image-2.6-flare', prompt: 'hi', quality: String(quality),
ratio: { w: Number(w), h: Number(h) },
}));
const [, entries] = batchIncrementUsagesSpy.mock.calls[0]!;
const output = (entries as Array<{ usageType: string; costOverride: number }>)
.find((entry) => entry.usageType.endsWith(':output'))!;
expect(output.usageType).toBe(`openai:gpt-image-2.6-flare:${quality}:${w}x${h}:output`);
// The metering conversion rounds fractional microcents upward.
expect(output.costOverride).toBeGreaterThanOrEqual(Number(tokens) * 3000);
expect(output.costOverride).toBeLessThanOrEqual(Number(tokens) * 3000 + 1);
});
});
@@ -56,8 +56,8 @@ interface OpenAIImageUsage {
/**
* OpenAI image generation provider for v2. Supports the GPT Image models
* (gpt-image-2, -2.5-sunburst, -2.5-flare), including image-to-image editing
* via `input_images` (the `images.edit` endpoint).
* (gpt-image-2 and the 2.5/2.6 Sunburst and Flare models), including
* image-to-image editing via `input_images` (the `images.edit` endpoint).
*/
export class OpenAiImageProvider implements IImageProvider {
#meteringService: AiMeteringService;
@@ -109,7 +109,7 @@ export class OpenAiImageProvider implements IImageProvider {
}
getDefaultModel(): string {
return 'gpt-image-2';
return 'gpt-image-2.6-flare';
}
async generate({
@@ -604,17 +604,36 @@ export class OpenAiImageProvider implements IImageProvider {
height: number,
quality?: string,
): number {
const factors = model.startsWith('gpt-image-2.5')
? OpenAiImageProvider.#GPT_IMAGE_2_5_LATENT_FACTORS
: OpenAiImageProvider.#GPT_IMAGE_2_LATENT_FACTORS;
const isGptImage26 = model.startsWith('gpt-image-2.6');
const factors =
isGptImage26 || model.startsWith('gpt-image-2.5')
? OpenAiImageProvider.#GPT_IMAGE_2_5_LATENT_FACTORS
: OpenAiImageProvider.#GPT_IMAGE_2_LATENT_FACTORS;
const factor = factors[quality ?? ''] ?? factors.medium;
const longEdge = Math.max(width, height);
const shortEdge = Math.min(width, height);
const shortLatent = Math.round((factor * shortEdge) / longEdge);
const shortGrid = (factor * shortEdge) / longEdge;
const floorGrid = Math.floor(shortGrid);
const shortLatent =
isGptImage26 && shortGrid - floorGrid === 0.5
? floorGrid + (floorGrid % 2)
: Math.round(shortGrid);
const latentW = width >= height ? factor : shortLatent;
const latentH = width >= height ? shortLatent : factor;
const baseArea = latentW * latentH;
return Math.ceil((baseArea * (2_000_000 + width * height)) / 4_000_000);
const tokens = Math.ceil(
(baseArea * (2_000_000 + width * height)) / 4_000_000,
);
// GPT Image 2.6 bills more tokens at low and medium quality.
const estimatedQuality =
quality && quality in factors ? quality : 'medium';
const multiplier =
estimatedQuality === 'low'
? 4.2
: estimatedQuality === 'medium'
? 3.3
: 1;
return isGptImage26 ? Math.round(tokens * multiplier) : tokens;
}
#estimateOutputCostFromTokens(
@@ -20,6 +20,46 @@
import type { IImageModel } from '../../types.js';
export const OPEN_AI_IMAGE_GENERATION_MODELS: IImageModel[] = [
{
puterId: 'openai:openai/gpt-image-2.6-sunburst',
id: 'gpt-image-2.6-sunburst',
aliases: ['openai/gpt-image-2.6-sunburst'],
name: 'GPT Image 2.6 Sunburst',
version: '2.6',
costs_currency: 'usd-cents',
index_cost_key: 'low:1024x1024',
costs: {
// Text tokens (per 1M tokens)
text_input: 500, // $5.00
text_cached_input: 125, // $1.25
// Image tokens (per 1M tokens)
image_input: 800, // $8.00
image_cached_input: 200, // $2.00
image_output: 3000, // $30.00
'low:1024x1024': 2.469,
},
allowedQualityLevels: ['low', 'medium', 'high', 'xhigh', 'max', 'auto'],
},
{
puterId: 'openai:openai/gpt-image-2.6-flare',
id: 'gpt-image-2.6-flare',
aliases: ['openai/gpt-image-2.6-flare'],
name: 'GPT Image 2.6 Flare',
version: '2.6',
costs_currency: 'usd-cents',
index_cost_key: 'low:1024x1024',
costs: {
// Text tokens (per 1M tokens)
text_input: 500, // $5.00
text_cached_input: 125, // $1.25
// Image tokens (per 1M tokens)
image_input: 800, // $8.00
image_cached_input: 200, // $2.00
image_output: 3000, // $30.00
'low:1024x1024': 2.469,
},
allowedQualityLevels: ['low', 'medium', 'high', 'xhigh', 'max', 'auto'],
},
{
puterId: 'openai:openai/gpt-image-2.5-sunburst',
id: 'gpt-image-2.5-sunburst',
+6 -6
View File
@@ -47,7 +47,7 @@ Without a model, these provider defaults apply:
| Provider | Default model |
|----------|---------------|
| OpenAI | `gpt-image-2` |
| OpenAI | `gpt-image-2.6-flare` |
| Gemini | `gemini-3.1-flash-image` |
| Cloudflare | `@cf/black-forest-labs/flux-1-schnell` |
| xAI | `grok-imagine-image` |
@@ -55,7 +55,7 @@ Without a model, these provider defaults apply:
| BytePlus | `seedream-5-0-lite-260128` |
| Kling | `kling-v3` |
When neither is supplied, OpenAI's `gpt-image-2` is used.
When neither is supplied, OpenAI's `gpt-image-2.6-flare` is used.
Discover the image catalog, including model IDs, aliases, provider names, and pricing metadata:
@@ -98,12 +98,12 @@ Raw base64 carries no MIME type of its own. When it cannot be detected from the
#### OpenAI Options
Available when `provider: 'openai'` or inferred from model (`gpt-image-2.5-sunburst`, `gpt-image-2.5-flare`, `gpt-image-2`):
Available when `provider: 'openai'` or inferred from model (`gpt-image-2.6-sunburst`, `gpt-image-2.6-flare`, `gpt-image-2.5-sunburst`, `gpt-image-2.5-flare`, `gpt-image-2`):
| Option | Type | Description |
|--------|------|-------------|
| `model` | `String` | Image model to use. Available: `'gpt-image-2.5-sunburst'`, `'gpt-image-2.5-flare'`, `'gpt-image-2'` (default). |
| `quality` | `String` | Image quality: `'high'`, `'medium'`, `'low'` (default: `'low'`); `gpt-image-2` also accepts `'auto'`, and the `gpt-image-2.5-*` models also accept `'xhigh'`, `'max'`, and `'auto'`. Case-insensitive; unrecognized values fall back to `'low'`. |
| `model` | `String` | Image model to use. Available: `'gpt-image-2.6-flare'` (default), `'gpt-image-2.6-sunburst'`, `'gpt-image-2.5-sunburst'`, `'gpt-image-2.5-flare'`, `'gpt-image-2'`. |
| `quality` | `String` | Image quality: `'high'`, `'medium'`, `'low'` (default: `'low'`); `gpt-image-2` also accepts `'auto'`, and the `gpt-image-2.5-*` and `gpt-image-2.6-*` models also accept `'xhigh'`, `'max'`, and `'auto'`. Case-insensitive; unrecognized values fall back to `'low'`. |
| `ratio` | `Object` | Aspect ratio with `w` and `h` properties. All GPT Image models accept dynamic sizes subject to provider limits (16-pixel steps, 3:1 maximum ratio, 3840-pixel edge cap) |
| `input_image` | `String` | An input image for image-to-image editing, as a URL or base64/data-URI. |
| `input_images` | `Array<String>` | Multiple input images (URL or base64/data-URI) for image-to-image editing. |
@@ -301,7 +301,7 @@ Other `upstream_*` codes mean the provider rejected the request or was unavailab
<script>
// Generate an image of a cat playing piano using a specific model and quality set to low
puter.ai.txt2img("a cat playing the piano", {
model: "gpt-image-2",
model: "gpt-image-2.6-flare",
quality: "low"
}).then((image)=>{
document.body.appendChild(image);
@@ -4,7 +4,7 @@
<script>
// Generate an image of a cat playing the piano using a specific model and quality set to low
puter.ai.txt2img("a cat playing the piano", {
model: "gpt-image-2",
model: "gpt-image-2.6-flare",
quality: "low"
}).then((image)=>{
document.body.appendChild(image);
+2 -2
View File
@@ -263,13 +263,13 @@
* @property {string} [prompt] Non-empty text description of the image to generate. Missing, blank,
* or non-string values reject with `prompt_required`.
* @property {string} [model] Image model to use (provider-specific). Without one, the provider's
* default applies: `'gpt-image-2'` (OpenAI, preferred when no provider is given),
* default applies: `'gpt-image-2.6-flare'` (OpenAI, preferred when no provider is given),
* `'gemini-3.1-flash-image'` (Gemini), `'@cf/black-forest-labs/flux-1-schnell'` (Cloudflare),
* `'grok-imagine-image'` (xAI), `'black-forest-labs/flux-schnell'` (Replicate),
* `'seedream-5-0-lite-260128'` (BytePlus), or `'kling-v3'` (Kling).
* @property {string} [quality] Image quality / output size tier. Interpretation is provider- and
* model-specific: OpenAI GPT models take `'high'` | `'medium'` | `'low'` (default `'low'`),
* `gpt-image-2` also accepts `'auto'`, and `gpt-image-2.5-sunburst` / `gpt-image-2.5-flare` also
* `gpt-image-2` also accepts `'auto'`, and the `gpt-image-2.5-*` / `gpt-image-2.6-*` models also
* accept `'xhigh'` | `'max'` | `'auto'`; Gemini takes an output size tier `'512'` | `'1K'` | `'2K'` |
* `'4K'` (availability varies by model); Kling takes `'1k'` (default) | `'2k'`, and `'4k'` on `kling-v3-omni`.
* @property {string} [resolution] xAI output resolution: `'1k'` (default) or `'2k'`; model-specific resolution tier on Replicate.