fix nano banana pro pricing and resolutions (#2501)
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled

* fix nano banana pro pricing and resolutions

* move gemini resolution map into models.ts

* require gemini 3 model for specific resolution and price logic

* fix usageamount for mp models
This commit is contained in:
P3il4
2026-02-23 11:39:48 +03:00
committed by GitHub
parent 0ee2ebd7b4
commit 779321d334
2 changed files with 48 additions and 18 deletions
@@ -24,7 +24,7 @@ import { Context } from '../../../../../util/context.js';
import { EventService } from '../../../../EventService.js';
import { MeteringService } from '../../../../MeteringService/MeteringService.js';
import { IGenerateParams, IImageModel, IImageProvider } from '../types.js';
import { TOGETHER_IMAGE_GENERATION_MODELS } from './models.js';
import { TOGETHER_IMAGE_GENERATION_MODELS, GEMINI_3_IMAGE_RESOLUTION_MAP } from './models.js';
const TOGETHER_DEFAULT_RATIO = { w: 1024, h: 1024 };
type TogetherGenerateParams = IGenerateParams & {
@@ -101,21 +101,28 @@ export class TogetherImageGenerationProvider implements IImageProvider {
throw new Error('actor not found in context');
}
const priceKey = '1MP';
const centsPerMP = selectedModel.costs[priceKey]; // hardcoded for now since all together ai models use this type of pricing
if ( centsPerMP === undefined ) {
throw new Error(`No pricing configured for model ${selectedModel.id}`);
const isGemini3 = selectedModel.id === 'togetherai:google/gemini-3-pro-image';
let costInMicroCents: number;
let usageAmount: number;
const qualityCostKey = isGemini3 && quality && selectedModel.costs[quality] !== undefined ? quality : undefined;
if ( qualityCostKey ) {
const centsPerImage = selectedModel.costs[qualityCostKey];
costInMicroCents = centsPerImage * 1_000_000;
usageAmount = 1;
} else {
const priceKey = '1MP';
const centsPerMP = selectedModel.costs[priceKey];
if ( centsPerMP === undefined ) {
throw new Error(`No pricing configured for model ${selectedModel.id}`);
}
const MP = (ratio.h * ratio.w) / 1_000_000;
costInMicroCents = centsPerMP * MP * 1_000_000;
usageAmount = MP;
}
const usageType = `${selectedModel.id}:${priceKey}`;
let MP = (ratio.h * ratio.w) / 1_000_000;
if ( quality ) {
// if quality its gemini 3 image, so price based on those K sizes as MP
MP = parseInt(quality[0]) ; // convert to microcents
}
const costInMicroCents = centsPerMP * MP * 1_000_000; // cost in microcents
const usageType = `${selectedModel.id}:${quality || '1MP'}`;
const usageAllowed = await this.#meteringService.hasEnoughCredits(actor, costInMicroCents);
@@ -123,7 +130,17 @@ export class TogetherImageGenerationProvider implements IImageProvider {
throw APIError.create('insufficient_funds');
}
const request = this.#buildRequest(prompt, { ...options, ratio, model: selectedModel.id.replace('togetherai:', '') }) as unknown as Together.Images.ImageGenerateParams;
// Resolve abstract aspect ratios to actual pixel dimensions for Gemini 3 Pro
let resolvedRatio = ratio;
if ( isGemini3 && quality ) {
const ratioKey = `${ratio.w}:${ratio.h}`;
const resolutionEntry = GEMINI_3_IMAGE_RESOLUTION_MAP[ratioKey]?.[quality];
if ( resolutionEntry ) {
resolvedRatio = resolutionEntry;
}
}
const request = this.#buildRequest(prompt, { ...options, ratio: resolvedRatio, model: selectedModel.id.replace('togetherai:', '') }) as unknown as Together.Images.ImageGenerateParams;
try {
const response = await this.#client.images.generate(request);
@@ -131,7 +148,7 @@ export class TogetherImageGenerationProvider implements IImageProvider {
throw new Error('Together AI response did not include image data');
}
this.#meteringService.incrementUsage(actor, usageType, MP, costInMicroCents);
this.#meteringService.incrementUsage(actor, usageType, usageAmount, costInMicroCents);
const first = response.data[0] as { url?: string; b64_json?: string };
const url = first.url || (first.b64_json ? `data:image/png;base64,${ first.b64_json}` : undefined);
@@ -237,7 +237,7 @@ export const TOGETHER_IMAGE_GENERATION_MODELS: IImageModel[] = [
aliases: ['gemini-3-pro-image', 'google/gemini-3-pro-image'],
name: 'gemini-3-pro-image (Together AI)',
costs_currency: 'usd-cents',
index_cost_key: '1MP',
index_cost_key: '1K',
allowedQualityLevels: ['1K', '2K', '4K'],
allowedRatios: [
{ w: 1, h: 1 },
@@ -251,7 +251,7 @@ export const TOGETHER_IMAGE_GENERATION_MODELS: IImageModel[] = [
{ w: 16, h: 9 },
{ w: 21, h: 9 },
],
costs: { '1MP': 13.51 },
costs: { '1K': 13.4, '2K': 13.4, '4K': 24 },
},
{
id: 'togetherai:google/imagen-4.0-fast',
@@ -317,3 +317,16 @@ export const TOGETHER_IMAGE_GENERATION_MODELS: IImageModel[] = [
costs: { '1MP': 7 },
},
];
export const GEMINI_3_IMAGE_RESOLUTION_MAP: Record<string, Record<string, { w: number; h: number }>> = {
'1:1': { '1K': { w: 1024, h: 1024 }, '2K': { w: 2048, h: 2048 }, '4K': { w: 4096, h: 4096 } },
'2:3': { '1K': { w: 848, h: 1264 }, '2K': { w: 1696, h: 2528 }, '4K': { w: 3392, h: 5096 } },
'3:2': { '1K': { w: 1264, h: 848 }, '2K': { w: 2528, h: 1696 }, '4K': { w: 5096, h: 3392 } },
'3:4': { '1K': { w: 896, h: 1200 }, '2K': { w: 1792, h: 2400 }, '4K': { w: 3584, h: 4800 } },
'4:3': { '1K': { w: 1200, h: 896 }, '2K': { w: 2400, h: 1792 }, '4K': { w: 4800, h: 3584 } },
'4:5': { '1K': { w: 928, h: 1152 }, '2K': { w: 1856, h: 2304 }, '4K': { w: 3712, h: 4608 } },
'5:4': { '1K': { w: 1152, h: 928 }, '2K': { w: 2304, h: 1856 }, '4K': { w: 4608, h: 3712 } },
'9:16': { '1K': { w: 768, h: 1376 }, '2K': { w: 1536, h: 2752 }, '4K': { w: 3072, h: 5504 } },
'16:9': { '1K': { w: 1376, h: 768 }, '2K': { w: 2752, h: 1536 }, '4K': { w: 5504, h: 3072 } },
'21:9': { '1K': { w: 1584, h: 672 }, '2K': { w: 3168, h: 1344 }, '4K': { w: 6336, h: 2688 } },
};