Files
promptfoo--promptfoo/test/providers/xai/image.test.ts
T
wehub-resource-sync 0d3cb498a3
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

858 lines
26 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { callOpenAiImageApi } from '../../../src/providers/openai/image';
import { getRequestTimeoutMs } from '../../../src/providers/shared';
import { createXAIImageProvider, XAIImageProvider } from '../../../src/providers/xai/image';
import { mockProcessEnv } from '../../util/utils';
vi.mock('../../../src/logger');
vi.mock('../../../src/providers/openai/image', async () => {
const actual = await vi.importActual('../../../src/providers/openai/image');
return {
...actual,
callOpenAiImageApi: vi.fn(),
};
});
describe('XAI Image Provider', () => {
const mockApiKey = 'test-api-key';
const mockPrompt = 'test prompt';
const mockSuccessResponse = {
data: {
created: 1234567890,
data: [
{
url: 'https://example.com/image.jpg',
},
],
},
cached: false,
status: 200,
statusText: 'OK',
};
const mockBase64Response = {
data: {
created: 1234567890,
data: [{ b64_json: 'base64EncodedImageData' }],
},
cached: false,
status: 200,
statusText: 'OK',
};
const mockCachedResponse = {
data: {
created: 1234567890,
data: [
{
url: 'https://example.com/image.jpg',
},
],
},
cached: true,
status: 200,
statusText: 'OK',
};
beforeEach(() => {
vi.resetAllMocks();
vi.clearAllMocks();
vi.mocked(callOpenAiImageApi).mockResolvedValue(mockSuccessResponse);
});
afterEach(() => {
vi.resetAllMocks();
});
describe('Provider creation and configuration', () => {
it('throws an error if no model name is provided', () => {
expect(() => createXAIImageProvider('xai:image:')).toThrow('Model name is required');
});
it('creates an xAI image provider with specified model', () => {
const provider = createXAIImageProvider('xai:image:grok-2-image');
expect(provider).toBeInstanceOf(XAIImageProvider);
expect(provider.id()).toBe('xai:image:grok-2-image');
});
it('supports the current Grok Imagine image model', () => {
const provider = createXAIImageProvider('xai:image:grok-imagine-image');
expect(provider).toBeInstanceOf(XAIImageProvider);
expect(provider.id()).toBe('xai:image:grok-imagine-image');
});
it('supports current Grok Imagine image aliases and pro model', () => {
// Verified against xAI /v1/image-generation-models.
expect(createXAIImageProvider('xai:image:grok-imagine-image-2026-03-02').id()).toBe(
'xai:image:grok-imagine-image-2026-03-02',
);
expect(createXAIImageProvider('xai:image:grok-imagine-image-quality').id()).toBe(
'xai:image:grok-imagine-image-quality',
);
expect(createXAIImageProvider('xai:image:grok-imagine-image-quality-latest').id()).toBe(
'xai:image:grok-imagine-image-quality-latest',
);
expect(createXAIImageProvider('xai:image:grok-imagine-image-quality-20260403').id()).toBe(
'xai:image:grok-imagine-image-quality-20260403',
);
expect(createXAIImageProvider('xai:image:grok-imagine-image-pro').id()).toBe(
'xai:image:grok-imagine-image-pro',
);
});
it('should create provider with correct defaults', () => {
const provider = new XAIImageProvider('grok-2-image');
expect(provider.config).toEqual({});
expect(provider.modelName).toBe('grok-2-image');
});
it('should use correct API URL', () => {
const provider = new XAIImageProvider('grok-2-image');
expect(provider.getApiUrlDefault()).toBe('https://api.x.ai/v1');
});
it('uses a regional API URL when configured', () => {
const provider = new XAIImageProvider('grok-imagine-image', {
config: { region: 'eu-west-1' },
});
// The OpenAI base reads `apiBaseUrl` directly, so the regional URL must be
// baked in at construction time — not just exposed via getApiUrlDefault().
expect(provider.getApiUrl()).toBe('https://eu-west-1.api.x.ai/v1');
expect(provider.getApiUrlDefault()).toBe('https://eu-west-1.api.x.ai/v1');
});
it('user-provided apiBaseUrl wins over region', () => {
const provider = new XAIImageProvider('grok-imagine-image', {
config: { region: 'eu-west-1', apiBaseUrl: 'https://my-proxy.example.com/v1' },
});
expect(provider.getApiUrl()).toBe('https://my-proxy.example.com/v1');
});
it('uses correct model mapping', () => {
const provider = new XAIImageProvider('grok-image', {
config: { apiKey: 'test-key' },
});
// The provider should map 'grok-image' to 'grok-2-image' internally
expect(provider).toBeInstanceOf(XAIImageProvider);
});
});
describe('Basic functionality', () => {
it('uses Grok Imagine generation options and reported cost when present', async () => {
vi.mocked(callOpenAiImageApi).mockResolvedValueOnce({
...mockSuccessResponse,
data: {
...mockSuccessResponse.data,
usage: { cost_in_usd_ticks: 200000000 },
},
});
const provider = new XAIImageProvider('grok-imagine-image', {
config: {
apiKey: mockApiKey,
aspect_ratio: '16:9',
resolution: '2k',
},
});
const result = await provider.callApi('Generate a cat');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
'https://api.x.ai/v1/images/generations',
{
model: 'grok-imagine-image',
prompt: 'Generate a cat',
n: 1,
response_format: 'url',
aspect_ratio: '16:9',
resolution: '2k',
},
{
'Content-Type': 'application/json',
Authorization: `Bearer ${mockApiKey}`,
},
getRequestTimeoutMs(),
);
expect(result.cost).toBe(0.02);
});
it('keeps the pro request slug while fallback pricing follows the quality redirect', async () => {
const provider = new XAIImageProvider('grok-imagine-image-pro', {
config: { apiKey: mockApiKey },
});
const result = await provider.callApi('Generate a cat');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
'https://api.x.ai/v1/images/generations',
{
model: 'grok-imagine-image-pro',
prompt: 'Generate a cat',
n: 1,
response_format: 'url',
},
{
'Content-Type': 'application/json',
Authorization: `Bearer ${mockApiKey}`,
},
getRequestTimeoutMs(),
);
expect(result.cost).toBe(0.05);
});
it('routes Grok Imagine quality aliases to the canonical model slug', async () => {
const provider = new XAIImageProvider('grok-imagine-image-quality-latest', {
config: { apiKey: mockApiKey },
});
const result = await provider.callApi('Generate a cat');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
'https://api.x.ai/v1/images/generations',
expect.objectContaining({ model: 'grok-imagine-image-quality' }),
expect.any(Object),
getRequestTimeoutMs(),
);
expect(result.cost).toBe(0.05);
});
it('preserves unknown Grok Imagine slugs instead of falling back to grok-2-image', async () => {
const provider = new XAIImageProvider('grok-imagine-image-future-preview', {
config: { apiKey: mockApiKey },
});
await provider.callApi('Generate a cat');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
'https://api.x.ai/v1/images/generations',
expect.objectContaining({ model: 'grok-imagine-image-future-preview' }),
expect.any(Object),
getRequestTimeoutMs(),
);
});
it('prices unknown Grok Imagine slugs at the quality tier, not grok-2-image', async () => {
const provider = new XAIImageProvider('grok-imagine-image-future-preview', {
config: { apiKey: mockApiKey },
});
const result = await provider.callApi('Generate a cat');
// Unknown grok-imagine-* slugs are priced like grok-imagine-image-quality
// ($0.05 at 1k), not the legacy grok-2-image rate ($0.07).
expect(result.cost).toBe(0.05);
});
it('supports the quality image model with resolution-sensitive fallback pricing', async () => {
const provider = new XAIImageProvider('grok-imagine-image-quality', {
config: { apiKey: mockApiKey, resolution: '2k' },
});
const result = await provider.callApi('Generate a cat');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
'https://api.x.ai/v1/images/generations',
{
model: 'grok-imagine-image-quality',
prompt: 'Generate a cat',
n: 1,
response_format: 'url',
resolution: '2k',
},
{
'Content-Type': 'application/json',
Authorization: `Bearer ${mockApiKey}`,
},
getRequestTimeoutMs(),
);
expect(result.cost).toBe(0.07);
});
it('uses the edits endpoint when image inputs are provided', async () => {
const provider = new XAIImageProvider('grok-imagine-image', {
config: {
apiKey: mockApiKey,
image: { url: 'https://example.com/source.png' },
mask: { url: 'https://example.com/mask.png' },
quality: 'high',
},
});
await provider.callApi('Render this as a pencil sketch');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
'https://api.x.ai/v1/images/edits',
{
model: 'grok-imagine-image',
prompt: 'Render this as a pencil sketch',
n: 1,
response_format: 'url',
quality: 'high',
image: { url: 'https://example.com/source.png' },
mask: { url: 'https://example.com/mask.png' },
},
{
'Content-Type': 'application/json',
Authorization: `Bearer ${mockApiKey}`,
},
getRequestTimeoutMs(),
);
});
it('includes documented source-image input pricing in fallback edit cost estimates', async () => {
const provider = new XAIImageProvider('grok-imagine-image-quality', {
config: {
apiKey: mockApiKey,
images: [
{ url: 'https://example.com/source-1.png' },
{ url: 'https://example.com/source-2.png' },
],
},
});
const result = await provider.callApi('Combine these source images');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
'https://api.x.ai/v1/images/edits',
{
model: 'grok-imagine-image-quality',
prompt: 'Combine these source images',
n: 1,
response_format: 'url',
images: [
{ url: 'https://example.com/source-1.png' },
{ url: 'https://example.com/source-2.png' },
],
},
{
'Content-Type': 'application/json',
Authorization: `Bearer ${mockApiKey}`,
},
getRequestTimeoutMs(),
);
expect(result.cost).toBe(0.07);
});
it('uses quality redirect pricing for deprecated pro edit fallback costs', async () => {
const provider = new XAIImageProvider('grok-imagine-image-pro', {
config: {
apiKey: mockApiKey,
image: { url: 'https://example.com/source.png' },
},
});
const result = await provider.callApi('Restyle this source image');
expect(result.cost).toBeCloseTo(0.06, 10);
});
it('should generate an image successfully', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey },
});
const result = await provider.callApi('Generate a cat');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
'https://api.x.ai/v1/images/generations',
{
model: 'grok-2-image',
prompt: 'Generate a cat',
n: 1,
response_format: 'url',
},
{
'Content-Type': 'application/json',
Authorization: `Bearer ${mockApiKey}`,
},
getRequestTimeoutMs(),
);
expect(result).toEqual({
output: '![Generate a cat](https://example.com/image.jpg)',
images: [{ data: 'https://example.com/image.jpg', mimeType: 'image/jpeg' }],
cached: false,
cost: 0.07, // xAI pricing: $0.07 per generated image
});
});
it('should use cached response', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey },
});
vi.mocked(callOpenAiImageApi).mockResolvedValue(mockCachedResponse);
const result = await provider.callApi('test prompt');
expect(result).toEqual({
output: '![test prompt](https://example.com/image.jpg)',
images: [{ data: 'https://example.com/image.jpg', mimeType: 'image/jpeg' }],
cached: true,
cost: 0,
});
});
it('should include all generated URL images in images array', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey, n: 2 },
});
vi.mocked(callOpenAiImageApi).mockResolvedValue({
data: {
data: [
{ url: 'https://example.com/image-1.jpg' },
{ url: 'https://example.com/image-2.jpg' },
],
},
cached: false,
status: 200,
statusText: 'OK',
});
const result = await provider.callApi('test prompt');
expect(result).toEqual({
output: '![test prompt](https://example.com/image-1.jpg)',
images: [
{ data: 'https://example.com/image-1.jpg', mimeType: 'image/jpeg' },
{ data: 'https://example.com/image-2.jpg', mimeType: 'image/jpeg' },
],
cached: false,
cost: 0.14,
});
});
it('should include latencyMs when available', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey },
});
vi.mocked(callOpenAiImageApi).mockResolvedValue({
...mockSuccessResponse,
latencyMs: 321,
});
const result = await provider.callApi('test prompt');
expect(result).toMatchObject({
latencyMs: 321,
});
});
it('should use XAI API endpoint', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey },
});
await provider.callApi('test prompt');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
'https://api.x.ai/v1/images/generations',
expect.any(Object),
expect.any(Object),
expect.any(Number),
);
});
it('should throw an error if API key is not set', async () => {
// Save original environment variables
const originalXaiKey = process.env.XAI_API_KEY;
const originalOpenAiKey = process.env.OPENAI_API_KEY;
// Clear all possible API key environment variables
mockProcessEnv({ XAI_API_KEY: undefined });
mockProcessEnv({ OPENAI_API_KEY: undefined });
try {
// Create provider with no API key in config or environment
const provider = new XAIImageProvider('grok-2-image');
// Attempt to call the API should throw an error
await expect(provider.callApi('Generate a cat')).rejects.toThrow(
'xAI API key is not set. Set the XAI_API_KEY environment variable or add `apiKey` to the provider config.',
);
} finally {
// Restore the original environment variables
if (originalXaiKey) {
mockProcessEnv({ XAI_API_KEY: originalXaiKey });
}
if (originalOpenAiKey) {
mockProcessEnv({ OPENAI_API_KEY: originalOpenAiKey });
}
}
});
it('should calculate correct cost', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey },
});
const result = await provider.callApi(mockPrompt);
expect(result.cost).toBe(0.07); // $0.07 per image
});
});
describe('Model name mapping', () => {
it('should map grok-image to grok-2-image', async () => {
const provider = new XAIImageProvider('grok-image', {
config: { apiKey: mockApiKey },
});
await provider.callApi('test prompt');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
model: 'grok-2-image',
}),
expect.any(Object),
expect.any(Number),
);
});
it('should use grok-2-image as default for unknown models', async () => {
const provider = new XAIImageProvider('unknown-model', {
config: { apiKey: mockApiKey },
});
await provider.callApi('test prompt');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
model: 'grok-2-image',
}),
expect.any(Object),
expect.any(Number),
);
});
});
describe('Error handling', () => {
it('should handle API errors', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey },
});
const errorResponse = {
data: { error: { message: 'API error message', type: 'api_error', code: 'error_code' } },
cached: false,
status: 400,
statusText: 'Bad Request',
};
vi.mocked(callOpenAiImageApi).mockResolvedValue(errorResponse);
const result = await provider.callApi('test prompt');
expect(result).toHaveProperty('error');
expect(result.error).toContain('API error: 400 Bad Request');
});
it('should handle HTTP errors', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey },
});
vi.mocked(callOpenAiImageApi).mockResolvedValue({
data: 'Error message',
cached: false,
status: 500,
statusText: 'Internal Server Error',
});
const result = await provider.callApi('test prompt');
expect(result).toHaveProperty('error');
expect(result.error).toContain('API error: 500 Internal Server Error');
});
it('should handle fetch errors', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey },
});
vi.mocked(callOpenAiImageApi).mockRejectedValue(new Error('Network error'));
const result = await provider.callApi('test prompt');
expect(result).toHaveProperty('error');
expect(result.error).toContain('API call error: Error: Network error');
});
it('should handle missing image URL in response', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey },
});
vi.mocked(callOpenAiImageApi).mockResolvedValue({
data: { data: [{}] },
cached: false,
status: 200,
statusText: 'OK',
});
const result = await provider.callApi('test prompt');
expect(result).toHaveProperty('error');
expect(result.error).toContain('No image URL found in response');
});
it('should handle non-200 API responses', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey },
});
vi.mocked(callOpenAiImageApi).mockResolvedValue({
data: { error: 'Invalid request' },
cached: false,
status: 400,
statusText: 'Bad Request',
});
const result = await provider.callApi(mockPrompt);
expect(result.error).toMatch(/API error: 400 Bad Request/);
});
});
describe('Response format handling', () => {
it('should handle base64 response format', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey, response_format: 'b64_json' },
});
vi.mocked(callOpenAiImageApi).mockResolvedValue(mockBase64Response);
const result = await provider.callApi('test prompt');
expect(result).toEqual({
output: 'data:image/png;base64,base64EncodedImageData',
images: [{ data: 'data:image/png;base64,base64EncodedImageData', mimeType: 'image/png' }],
cached: false,
isBase64: true,
format: 'json',
cost: 0.07,
});
// Verify the request included the response_format parameter
expect(callOpenAiImageApi).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
response_format: 'b64_json',
}),
expect.any(Object),
expect.any(Number),
);
});
it('should handle missing base64 data in response', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey, response_format: 'b64_json' },
});
vi.mocked(callOpenAiImageApi).mockResolvedValue({
data: { data: [{}] },
cached: false,
status: 200,
statusText: 'OK',
});
const result = await provider.callApi('test prompt');
expect(result).toHaveProperty('error');
expect(result.error).toContain('No base64 image data found in response');
});
it('should handle custom response format', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: {
apiKey: mockApiKey,
response_format: 'b64_json',
},
});
await provider.callApi(mockPrompt);
expect(callOpenAiImageApi).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
response_format: 'b64_json',
}),
expect.any(Object),
expect.any(Number),
);
});
});
describe('Configuration options', () => {
it('should handle number of images parameter', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey, n: 2 },
});
await provider.callApi('test prompt');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
n: 2,
}),
expect.any(Object),
expect.any(Number),
);
});
it('should handle user parameter', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey, user: 'test-user' },
});
await provider.callApi('test prompt');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
user: 'test-user',
}),
expect.any(Object),
expect.any(Number),
);
});
it('should include custom headers when provided', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: {
apiKey: mockApiKey,
headers: {
'X-Custom-Header': 'custom-value',
},
},
});
await provider.callApi('test prompt');
expect(callOpenAiImageApi).toHaveBeenCalledWith(
expect.any(String),
expect.any(Object),
expect.objectContaining({
'X-Custom-Header': 'custom-value',
}),
expect.any(Number),
);
});
it('should merge prompt config with provider config', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey, n: 1 },
});
const context = {
prompt: {
raw: 'test prompt',
config: { n: 2 },
label: 'test',
},
vars: {},
};
await provider.callApi('test prompt', context);
expect(callOpenAiImageApi).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
n: 2,
}),
expect.any(Object),
expect.any(Number),
);
});
});
describe('Provider methods', () => {
it('generates correct id() for the provider', () => {
const provider = new XAIImageProvider('grok-2-image');
expect(provider.id()).toBe('xai:image:grok-2-image');
});
it('returns readable toString() description', () => {
const provider = new XAIImageProvider('grok-2-image');
expect(provider.toString()).toBe('[xAI Image Provider grok-2-image]');
});
it('returns correct default API URL', () => {
const provider = new XAIImageProvider('grok-2-image');
expect(provider.getApiUrlDefault()).toBe('https://api.x.ai/v1');
});
});
describe('createXAIImageProvider factory function', () => {
it('should create provider instance', () => {
const provider = createXAIImageProvider('xai:image:grok-2-image');
expect(provider).toBeInstanceOf(XAIImageProvider);
});
it('should throw error if model name missing', () => {
expect(() => createXAIImageProvider('xai:image:')).toThrow('Model name is required');
});
it('should pass through options correctly', () => {
const options = {
config: { apiKey: 'test-key', n: 2 },
};
const provider = createXAIImageProvider('xai:image:grok-2-image', options);
expect(provider.config).toEqual(options.config);
});
});
describe('Cost calculation for images', () => {
it('should calculate cost correctly for single image', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey },
});
const result = await provider.callApi('test prompt');
expect(result.cost).toBe(0.07);
});
it('should not charge for cached responses', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey },
});
vi.mocked(callOpenAiImageApi).mockResolvedValue(mockCachedResponse);
const result = await provider.callApi('test prompt');
expect(result.cost).toBe(0);
expect(result.cached).toBe(true);
});
it('should calculate cost for multiple images', async () => {
const provider = new XAIImageProvider('grok-2-image', {
config: { apiKey: mockApiKey, n: 3 },
});
const multiImageResponse = {
data: {
created: 1234567890,
data: [
{ url: 'https://example.com/image1.jpg' },
{ url: 'https://example.com/image2.jpg' },
{ url: 'https://example.com/image3.jpg' },
],
},
cached: false,
status: 200,
statusText: 'OK',
};
vi.mocked(callOpenAiImageApi).mockResolvedValue(multiImageResponse);
const result = await provider.callApi('test prompt');
expect(result.cost).toBeCloseTo(0.21, 2); // 3 images * $0.07
});
});
});