Skip to content

Commit a030fcd

Browse files
committed
Add xAI image generation provider integration
1 parent 54dd60d commit a030fcd

4 files changed

Lines changed: 137 additions & 1 deletion

File tree

src/backend/src/services/MeteringService/costMaps/xaiCostMap.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,7 @@ export const XAI_COST_MAP = {
5050
// Grok 2
5151
'xai:grok-2:prompt_tokens': 200,
5252
'xai:grok-2:completion-tokens': 1000,
53-
};
53+
54+
// Grok Image
55+
'xai:grok-2-image:output': 7_000_000,
56+
};

src/backend/src/services/ai/image/AIImageGenerationService.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { MeteringService } from '../../MeteringService/MeteringService.js';
3030
import { GeminiImageGenerationProvider } from './providers/GeminiImageGenerationProvider/GeminiImageGenerationProvider.js';
3131
import { OpenAiImageGenerationProvider } from './providers/OpenAiImageGenerationProvider/OpenAiImageGenerationProvider.js';
3232
import { TogetherImageGenerationProvider } from './providers/TogetherImageGenerationProvider/TogetherImageGenerationProvider.js';
33+
import { XAIImageGenerationProvider } from './providers/XAIImageGenerationProvider/XAIImageGenerationProvider.js';
3334
import { IGenerateParams, IImageModel, IImageProvider } from './providers/types.js';
3435

3536
export class AIImageGenerationService extends BaseService {
@@ -108,6 +109,11 @@ export class AIImageGenerationService extends BaseService {
108109
this.#providers['together-image-generation'] = new TogetherImageGenerationProvider({ apiKey: togetherConfig.apiKey || togetherConfig.secret_key }, this.meteringService, this.errorService, this.eventService);
109110
}
110111

112+
const xaiConfig = this.config.providers?.['xai-image-generation'] || this.config.providers?.['xai'] || this.global_config?.services?.['xai'];
113+
if ( xaiConfig && (xaiConfig.apiKey || xaiConfig.secret_key) ) {
114+
this.#providers['xai-image-generation'] = new XAIImageGenerationProvider({ apiKey: xaiConfig.apiKey || xaiConfig.secret_key }, this.meteringService, this.errorService);
115+
}
116+
111117
// emit event for extensions to add providers
112118
const extensionProviders = {} as Record<string, IImageProvider>;
113119
await this.eventService.emit('ai.image.registerProviders', extensionProviders);
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (C) 2024-present Puter Technologies Inc.
3+
*
4+
* This file is part of Puter.
5+
*
6+
* Puter is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
import { OpenAI } from 'openai';
21+
import APIError from '../../../../../api/APIError.js';
22+
import { ErrorService } from '../../../../../modules/core/ErrorService.js';
23+
import { Context } from '../../../../../util/context.js';
24+
import { MeteringService } from '../../../../MeteringService/MeteringService.js';
25+
import { IGenerateParams, IImageModel, IImageProvider } from '../types.js';
26+
import { XAI_IMAGE_GENERATION_MODELS } from './models.js';
27+
28+
const DEFAULT_MODEL = 'grok-2-image';
29+
const PRICE_KEY = 'output';
30+
31+
export class XAIImageGenerationProvider implements IImageProvider {
32+
#client: OpenAI;
33+
#meteringService: MeteringService;
34+
#errors: ErrorService;
35+
36+
constructor (config: { apiKey: string }, meteringService: MeteringService, errorService: ErrorService) {
37+
if ( ! config.apiKey ) {
38+
throw new Error('xAI image generation requires an API key');
39+
}
40+
41+
this.#meteringService = meteringService;
42+
this.#errors = errorService;
43+
this.#client = new OpenAI({
44+
apiKey: config.apiKey,
45+
baseURL: 'https://api.x.ai/v1',
46+
});
47+
}
48+
49+
models (): IImageModel[] {
50+
return XAI_IMAGE_GENERATION_MODELS;
51+
}
52+
53+
getDefaultModel (): string {
54+
return DEFAULT_MODEL;
55+
}
56+
57+
async generate (params: IGenerateParams): Promise<string> {
58+
const { prompt, test_mode } = params;
59+
let { model } = params;
60+
61+
const selectedModel = this.#getModel(model);
62+
63+
if ( test_mode ) {
64+
return 'https://puter-sample-data.puter.site/image_example.png';
65+
}
66+
67+
if ( typeof prompt !== 'string' || prompt.trim().length === 0 ) {
68+
throw new Error('`prompt` must be a non-empty string');
69+
}
70+
71+
const actor = Context.get('actor');
72+
const user_private_uid = actor?.private_uid ?? 'UNKNOWN';
73+
if ( user_private_uid === 'UNKNOWN' ) {
74+
this.#errors.report('xai-image-generation:unknown-user', {
75+
message: 'failed to get a user ID for an xAI request',
76+
alarm: true,
77+
trace: true,
78+
});
79+
}
80+
81+
const priceInCents = selectedModel.costs[PRICE_KEY];
82+
const costInMicroCents = priceInCents * 1_000_000;
83+
const usageAllowed = await this.#meteringService.hasEnoughCredits(actor, costInMicroCents);
84+
85+
if ( ! usageAllowed ) {
86+
throw APIError.create('insufficient_funds');
87+
}
88+
89+
const response = await this.#client.images.generate({
90+
model: selectedModel.id,
91+
prompt,
92+
user: user_private_uid,
93+
});
94+
95+
const first = response.data?.[0] as { url?: string; b64_json?: string } | undefined;
96+
const url = first?.url || (first?.b64_json ? `data:image/png;base64,${ first.b64_json}` : undefined);
97+
98+
if ( ! url ) {
99+
throw new Error('Failed to extract image URL from xAI response');
100+
}
101+
102+
this.#meteringService.incrementUsage(actor, `xai:${selectedModel.id}:${PRICE_KEY}`, 1, costInMicroCents);
103+
104+
return url;
105+
}
106+
107+
#getModel (model?: string) {
108+
const models = this.models();
109+
const found = models.find(m => m.id === model || m.aliases?.includes(model ?? ''));
110+
return found || models.find(m => m.id === DEFAULT_MODEL)!;
111+
}
112+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { IImageModel } from '../types';
2+
3+
export const XAI_IMAGE_GENERATION_MODELS: IImageModel[] = [
4+
{
5+
id: 'grok-2-image',
6+
aliases: ['grok-image'],
7+
name: 'Grok 2 Image',
8+
version: '1.0',
9+
costs_currency: 'usd-cents',
10+
index_cost_key: 'output',
11+
costs: {
12+
output: 7, // $0.07 per image
13+
},
14+
},
15+
];

0 commit comments

Comments
 (0)