mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-05 21:46:51 +00:00
feat(ai): add GPT Image 2.6 and default to Flare
This commit is contained in:
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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user