import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { calculateXAICost, createXAIProvider, GROK_3_MINI_MODELS, GROK_4_MODELS, GROK_REASONING_EFFORT_MODELS, GROK_REASONING_MODELS, getXAICostInUsd, XAI_CHAT_MODELS, } from '../../../src/providers/xai/chat'; import type { ProviderOptions } from '../../../src/types/providers'; // Mock only external dependencies - NOT the OpenAiChatCompletionProvider class vi.mock('../../../src/logger'); const mockFetchWithCache = vi.hoisted(() => vi.fn()); vi.mock('../../../src/cache', async (importOriginal) => { return { ...(await importOriginal()), fetchWithCache: (...args: any[]) => mockFetchWithCache(...args), getCache: vi.fn(), isCacheEnabled: vi.fn().mockReturnValue(false), }; }); describe('xAI Chat Provider', () => { beforeEach(() => { vi.clearAllMocks(); // Default mock for fetchWithCache - successful response mockFetchWithCache.mockResolvedValue({ data: { choices: [{ message: { content: 'Mock response' } }], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }, }, cached: false, status: 200, statusText: 'OK', }); }); afterEach(() => { vi.resetAllMocks(); }); describe('Provider creation and configuration', () => { it('throws an error if no model name is provided', () => { expect(() => createXAIProvider('xai:')).toThrow('Model name is required'); }); it('creates an xAI provider with specified model', () => { const provider = createXAIProvider('xai:grok-2') as any; expect(provider.id()).toBe('xai:grok-2'); expect(provider.modelName).toBe('grok-2'); expect(typeof provider.toString).toBe('function'); }); it('sets the correct API base URL and API key environment variable', () => { const provider = createXAIProvider('xai:grok-2') as any; expect(provider.config.apiBaseUrl).toBe('https://api.x.ai/v1'); expect(provider.config.apiKeyEnvar).toBe('XAI_API_KEY'); }); it('uses region-specific API base URL when region is provided', () => { const provider = createXAIProvider('xai:grok-2', { config: { config: { region: 'eu-west-1', }, }, }) as any; expect(provider.config.apiBaseUrl).toBe('https://eu-west-1.api.x.ai/v1'); expect(provider.config.apiKeyEnvar).toBe('XAI_API_KEY'); }); it('merges provided options with xAI-specific config', () => { const options: ProviderOptions = { config: { temperature: 0.7, max_tokens: 100, }, id: 'custom-id', }; const provider = createXAIProvider('xai:grok-2', options) as any; expect(provider.config.apiBaseUrl).toBe('https://api.x.ai/v1'); expect(provider.config.apiKeyEnvar).toBe('XAI_API_KEY'); expect(provider.config.temperature).toBe(0.7); expect(provider.config.max_tokens).toBe(100); }); it('stores originalConfig during initialization', () => { const config = { config: { search_parameters: { mode: 'test' }, region: 'eu-west-1', }, }; const provider = createXAIProvider('xai:grok-2', { config }) as any; expect(provider.originalConfig).toEqual(config.config); }); }); describe('supported models', () => { it('includes Grok 4.3 and 4.20 in the reasoning and Grok-4 parameter-restriction lists', () => { expect(XAI_CHAT_MODELS).toEqual( expect.arrayContaining([ expect.objectContaining({ id: 'grok-4.3' }), expect.objectContaining({ id: 'grok-4.20-0309-reasoning' }), expect.objectContaining({ id: 'grok-4.20-0309-non-reasoning' }), expect.objectContaining({ id: 'grok-4.20-multi-agent-0309' }), ]), ); expect(GROK_REASONING_MODELS).toEqual( expect.arrayContaining(['grok-4.3', 'grok-4.3-latest', 'grok-4.20-reasoning', 'grok-4.20']), ); expect(GROK_4_MODELS).toEqual( expect.arrayContaining([ 'grok-4.3', 'grok-4.20-reasoning', 'grok-4.20-non-reasoning', 'grok-4.20-multi-agent', ]), ); }); it('tracks a reviewed snapshot of public text model ids reported by xAI /v1/language-models', () => { // Maintenance note: // This is a point-in-time snapshot of xAI public text model ids. // Review and update periodically (or whenever models are added/renamed/deprecated) // against xAI /v1/language-models. const currentPublicTextModelIds = [ 'grok-3', 'grok-3-mini', 'grok-4-0709', 'grok-4-1-fast-non-reasoning', 'grok-4-1-fast-reasoning', 'grok-4-fast-non-reasoning', 'grok-4-fast-reasoning', 'grok-4.20-0309-non-reasoning', 'grok-4.20-0309-reasoning', 'grok-4.20-multi-agent-0309', 'grok-4.3', 'grok-code-fast-1', ]; const trackedModelIds = XAI_CHAT_MODELS.flatMap((model) => [ model.id, ...(model.aliases ?? []), ]); for (const modelId of currentPublicTextModelIds) { expect(trackedModelIds).toContain(modelId); } }); }); describe('Provider methods', () => { it('generates correct id() for the provider', () => { const provider = createXAIProvider('xai:grok-3-beta'); expect(provider.id()).toBe('xai:grok-3-beta'); }); it('returns readable toString() description', () => { const provider = createXAIProvider('xai:grok-3-mini-beta'); expect(provider.toString()).toBe('[xAI Provider grok-3-mini-beta]'); }); it('serializes properly with toJSON() and masks API key', () => { const provider = createXAIProvider('xai:grok-3-beta', { config: { temperature: 0.7, } as any, }); expect(provider.toJSON).toBeDefined(); const json = provider.toJSON!(); expect(json).toMatchObject({ provider: 'xai', model: 'grok-3-beta', }); // Verify API key is not exposed in serialized output expect(json.config?.apiKey).toBeUndefined(); }); }); describe('Reported cost handling', () => { it('converts xAI cost ticks to USD', () => { expect(getXAICostInUsd({ cost_in_usd_ticks: 37_756_000 })).toBe(0.0037756); expect(getXAICostInUsd()).toBeUndefined(); }); }); describe('Search parameters handling', () => { it('renders search_parameters with context variables', async () => { const provider = createXAIProvider('xai:grok-3-beta', { config: { config: { search_parameters: { mode: '{{mode}}', filter: '{{filter}}' }, }, }, }); const result = await (provider as any).getOpenAiBody('test prompt', { vars: { mode: 'advanced', filter: 'latest', }, }); expect(result.body.search_parameters).toEqual({ mode: 'advanced', filter: 'latest', }); }); it('includes search_parameters in API body when defined', async () => { const provider = createXAIProvider('xai:grok-3-beta', { config: { config: { search_parameters: { mode: 'test' }, }, }, }); const result = await (provider as any).getOpenAiBody('test prompt'); expect(result.body.search_parameters).toEqual({ mode: 'test' }); }); it('does not include search_parameters when undefined and preserves original configuration', async () => { const provider = createXAIProvider('xai:grok-3-beta'); const result = await (provider as any).getOpenAiBody('test prompt'); expect(result.body.search_parameters).toBeUndefined(); // Test preserving original config const searchParams = { mode: 'test', filter: 'all' }; const providerWithParams = createXAIProvider('xai:grok-3-beta', { config: { config: { search_parameters: searchParams, }, }, }) as any; expect(providerWithParams.originalConfig.search_parameters).toEqual(searchParams); }); }); describe('Temperature zero handling', () => { it('should correctly send temperature: 0 in the request body', async () => { // Test that temperature: 0 is correctly sent (not filtered out by falsy check) const provider = createXAIProvider('xai:grok-3-beta', { config: { temperature: 0, } as any, }); const result = await (provider as any).getOpenAiBody('test prompt'); // temperature: 0 should be present in the request body expect(result.body.temperature).toBe(0); expect('temperature' in result.body).toBe(true); }); }); describe('Model type detection and capabilities', () => { it('identifies reasoning models correctly', () => { expect(GROK_3_MINI_MODELS).toContain('grok-3-mini-beta'); expect(GROK_3_MINI_MODELS).toContain('grok-3-mini-fast-beta'); expect(GROK_3_MINI_MODELS).not.toContain('grok-2-1212'); }); it('identifies Grok Build and its Grok Code Fast aliases as reasoning models', () => { expect(GROK_REASONING_MODELS).toContain('grok-build-0.1'); expect(GROK_REASONING_MODELS).toContain('grok-code-fast-1'); expect(GROK_REASONING_MODELS).toContain('grok-code-fast'); expect(GROK_REASONING_MODELS).toContain('grok-code-fast-1-0825'); }); }); describe('Reasoning models configuration', () => { it('supports reasoning effort parameters for mini models', () => { // Test high reasoning effort const provider = createXAIProvider('xai:grok-3-mini-beta', { config: { config: { reasoning_effort: 'high', }, }, }) as any; expect(provider.config.apiBaseUrl).toBe('https://api.x.ai/v1'); expect(provider.config.apiKeyEnvar).toBe('XAI_API_KEY'); // Test low reasoning effort const provider2 = createXAIProvider('xai:grok-3-mini-beta', { config: { config: { reasoning_effort: 'low', }, }, }) as any; expect(provider2.config.apiKeyEnvar).toBe('XAI_API_KEY'); }); it('handles multiple configuration options for mini models', () => { const options: ProviderOptions = { config: { config: { reasoning_effort: 'high', region: 'us-east-1', }, temperature: 0.5, max_tokens: 1000, } as any, }; const provider = createXAIProvider('xai:grok-3-mini-beta', options) as any; expect(provider.config.apiBaseUrl).toBe('https://us-east-1.api.x.ai/v1'); expect(provider.config.temperature).toBe(0.5); expect(provider.config.max_tokens).toBe(1000); }); }); describe('Grok-4 specific functionality', () => { it('recognizes Grok 4.3 models as reasoning models', () => { const provider = createXAIProvider('xai:grok-4.3') as any; expect(provider.isReasoningModel()).toBe(true); const providerAlias = createXAIProvider('xai:grok-4.3-latest') as any; expect(providerAlias.isReasoningModel()).toBe(true); }); it('recognizes Grok-4 models as reasoning models', () => { const provider = createXAIProvider('xai:grok-4-0709') as any; expect(provider.isReasoningModel()).toBe(true); const providerAlias = createXAIProvider('xai:grok-4') as any; expect(providerAlias.isReasoningModel()).toBe(true); }); it('does not support reasoning_effort for Grok-4', () => { // xAI's chat-completions endpoint rejects `reasoning_effort` on legacy // Grok-4 family slugs even after the retirement-redirect to grok-4.3 is // documented; the parameter is silently stripped before sending. const provider = createXAIProvider('xai:grok-4-0709') as any; expect(provider.supportsReasoningEffort()).toBe(false); }); it('preserves reasoning_effort for Grok 4.3 chat requests', async () => { const provider = createXAIProvider('xai:grok-4.3') as any; const result = await provider.getOpenAiBody('test prompt', { prompt: { config: { reasoning_effort: 'high', presence_penalty: 0.5, frequency_penalty: 0.7, stop: ['\\n'], }, }, }); expect(provider.supportsReasoningEffort()).toBe(true); expect(result.body.reasoning_effort).toBe('high'); expect(result.body.presence_penalty).toBeUndefined(); expect(result.body.frequency_penalty).toBeUndefined(); expect(result.body.stop).toBeUndefined(); }); it('filters unsupported parameters for Grok-4 aliases', async () => { const provider = createXAIProvider('xai:grok-4') as any; const mockContext = { prompt: { config: { presence_penalty: 0.5, frequency_penalty: 0.7, stop: ['\\n'], reasoning_effort: 'high', temperature: 0.8, }, }, }; const result = await provider.getOpenAiBody('test prompt', mockContext); // xAI's chat-completions endpoint rejects every one of these on the // Grok-4 family, so they are stripped before send. expect(result.body.presence_penalty).toBeUndefined(); expect(result.body.frequency_penalty).toBeUndefined(); expect(result.body.stop).toBeUndefined(); expect(result.body.reasoning_effort).toBeUndefined(); expect(result.body.temperature).toBe(0.8); }); it('filters unsupported parameters for Grok 4.1 Fast models', async () => { const provider = createXAIProvider('xai:grok-4-1-fast-reasoning') as any; const mockContext = { prompt: { config: { presence_penalty: 0.5, frequency_penalty: 0.7, stop: ['\\n'], reasoning_effort: 'high', temperature: 0.7, max_completion_tokens: 2048, }, }, }; const result = await provider.getOpenAiBody('test prompt', mockContext); expect(result.body.presence_penalty).toBeUndefined(); expect(result.body.frequency_penalty).toBeUndefined(); expect(result.body.stop).toBeUndefined(); expect(result.body.reasoning_effort).toBeUndefined(); expect(result.body.temperature).toBe(0.7); expect(result.body.max_completion_tokens).toBe(2048); }); it('filters unsupported parameters for Grok 4 Fast models', async () => { const provider = createXAIProvider('xai:grok-4-fast-reasoning') as any; const mockContext = { prompt: { config: { presence_penalty: 0.5, frequency_penalty: 0.7, stop: ['\\n'], reasoning_effort: 'high', temperature: 0.7, }, }, }; const result = await provider.getOpenAiBody('test prompt', mockContext); expect(result.body.presence_penalty).toBeUndefined(); expect(result.body.frequency_penalty).toBeUndefined(); expect(result.body.stop).toBeUndefined(); expect(result.body.reasoning_effort).toBeUndefined(); expect(result.body.temperature).toBe(0.7); }); it('filters unsupported parameters for non-reasoning variants', async () => { const provider = createXAIProvider('xai:grok-4-1-fast-non-reasoning') as any; const mockContext = { prompt: { config: { presence_penalty: 0.5, frequency_penalty: 0.7, stop: ['\\n'], temperature: 0.5, }, }, }; const result = await provider.getOpenAiBody('test prompt', mockContext); // These should be filtered out for Grok 4.1 Fast non-reasoning expect(result.body.presence_penalty).toBeUndefined(); expect(result.body.frequency_penalty).toBeUndefined(); expect(result.body.stop).toBeUndefined(); // Temperature should still be present expect(result.body.temperature).toBe(0.5); }); }); describe('Model constants', () => { it('includes Grok-4 in reasoning models list', () => { expect(GROK_REASONING_MODELS).toContain('grok-4-0709'); expect(GROK_REASONING_MODELS).toContain('grok-4'); expect(GROK_REASONING_MODELS).toContain('grok-4-latest'); }); it('includes Grok 4.3 in reasoning models list', () => { expect(GROK_REASONING_MODELS).toContain('grok-4.3'); expect(GROK_REASONING_MODELS).toContain('grok-4.3-latest'); }); it('includes Grok 4.1 Fast reasoning models in reasoning models list', () => { expect(GROK_REASONING_MODELS).toContain('grok-4-1-fast-reasoning'); expect(GROK_REASONING_MODELS).toContain('grok-4-1-fast'); expect(GROK_REASONING_MODELS).toContain('grok-4-1-fast-latest'); expect(GROK_REASONING_MODELS).toContain('grok-4-1-fast-reasoning-latest'); }); it('includes Grok 4 Fast reasoning models in reasoning models list', () => { expect(GROK_REASONING_MODELS).toContain('grok-4-fast-reasoning'); expect(GROK_REASONING_MODELS).toContain('grok-4-fast'); expect(GROK_REASONING_MODELS).toContain('grok-4-fast-latest'); expect(GROK_REASONING_MODELS).toContain('grok-4-fast-reasoning-latest'); }); it('does NOT include non-reasoning variants in reasoning models list', () => { expect(GROK_REASONING_MODELS).not.toContain('grok-4-1-fast-non-reasoning'); expect(GROK_REASONING_MODELS).not.toContain('grok-4-1-fast-non-reasoning-latest'); expect(GROK_REASONING_MODELS).not.toContain('grok-4-fast-non-reasoning'); expect(GROK_REASONING_MODELS).not.toContain('grok-4-fast-non-reasoning-latest'); }); it('includes Grok 4.3 and Grok 4-family models in GROK_4_MODELS', () => { expect(GROK_4_MODELS).toContain('grok-4.3'); expect(GROK_4_MODELS).toContain('grok-4.3-latest'); expect(GROK_4_MODELS).toContain('grok-4-1-fast-reasoning'); expect(GROK_4_MODELS).toContain('grok-4-1-fast'); expect(GROK_4_MODELS).toContain('grok-4-1-fast-latest'); expect(GROK_4_MODELS).toContain('grok-4-1-fast-reasoning-latest'); expect(GROK_4_MODELS).toContain('grok-4-1-fast-non-reasoning'); expect(GROK_4_MODELS).toContain('grok-4-1-fast-non-reasoning-latest'); expect(GROK_4_MODELS).toContain('grok-4-fast-reasoning'); expect(GROK_4_MODELS).toContain('grok-4-fast'); expect(GROK_4_MODELS).toContain('grok-4-fast-latest'); expect(GROK_4_MODELS).toContain('grok-4-fast-reasoning-latest'); expect(GROK_4_MODELS).toContain('grok-4-fast-non-reasoning'); expect(GROK_4_MODELS).toContain('grok-4-fast-non-reasoning-latest'); }); it('does not include Grok-4 in reasoning effort models list', () => { expect(GROK_REASONING_EFFORT_MODELS).not.toContain('grok-4-0709'); expect(GROK_REASONING_EFFORT_MODELS).not.toContain('grok-4'); expect(GROK_REASONING_EFFORT_MODELS).not.toContain('grok-4-latest'); }); it('includes Grok 4.3 in reasoning effort models list', () => { expect(GROK_REASONING_EFFORT_MODELS).toContain('grok-4.3'); expect(GROK_REASONING_EFFORT_MODELS).toContain('grok-4.3-latest'); }); it('includes Grok-3 mini models in reasoning effort models list', () => { expect(GROK_REASONING_EFFORT_MODELS).toContain('grok-3-mini-beta'); expect(GROK_REASONING_EFFORT_MODELS).toContain('grok-3-mini-fast-beta'); }); it('includes Grok-4 in XAI_CHAT_MODELS with correct pricing', () => { const grok4 = XAI_CHAT_MODELS.find((m) => m.id === 'grok-4-0709'); expect(grok4).toBeDefined(); expect(grok4?.cost?.input).toBeDefined(); expect(grok4?.cost?.output).toBeDefined(); }); it('includes Grok 4.3 in XAI_CHAT_MODELS with API-sourced pricing', () => { const grok43 = XAI_CHAT_MODELS.find((m) => m.id === 'grok-4.3'); expect(grok43).toBeDefined(); expect(grok43?.aliases).toContain('grok-4.3-latest'); // Verified against xAI /v1/language-models/grok-4.3 (12500/25000/2000 ticks). expect(grok43?.cost?.input).toBe(1.25 / 1e6); expect(grok43?.cost?.output).toBe(2.5 / 1e6); expect(grok43?.cost?.cache_read).toBe(0.2 / 1e6); }); it('includes Grok 4.20 family with API-sourced pricing', () => { for (const modelId of [ 'grok-4.20-0309-reasoning', 'grok-4.20-0309-non-reasoning', 'grok-4.20-multi-agent-0309', ]) { const model = XAI_CHAT_MODELS.find((candidate) => candidate.id === modelId); expect(model?.cost?.input).toBe(1.25 / 1e6); expect(model?.cost?.output).toBe(2.5 / 1e6); expect(model?.cost?.cache_read).toBe(0.2 / 1e6); } }); it('includes Grok 4.1 Fast in XAI_CHAT_MODELS with correct pricing', () => { const grok41Fast = XAI_CHAT_MODELS.find((m) => m.id === 'grok-4-1-fast-reasoning'); expect(grok41Fast).toBeDefined(); expect(grok41Fast?.cost?.input).toBeDefined(); expect(grok41Fast?.cost?.output).toBeDefined(); }); it('includes Grok 4 Fast in XAI_CHAT_MODELS with correct pricing', () => { const grok4Fast = XAI_CHAT_MODELS.find((m) => m.id === 'grok-4-fast-reasoning'); expect(grok4Fast).toBeDefined(); expect(grok4Fast?.cost?.input).toBeDefined(); expect(grok4Fast?.cost?.output).toBeDefined(); }); it('includes all Grok-3 mini aliases in reasoning constants', () => { // Verify base models expect(GROK_3_MINI_MODELS).toContain('grok-3-mini-beta'); expect(GROK_3_MINI_MODELS).toContain('grok-3-mini-fast-beta'); // Verify aliases exist in XAI_CHAT_MODELS const miniModel = XAI_CHAT_MODELS.find((m) => m.id === 'grok-3-mini-beta'); expect(miniModel?.aliases).toContain('grok-3-mini'); expect(miniModel?.aliases).toContain('grok-3-mini-latest'); }); it('recognizes Grok-3 mini aliases as reasoning models', () => { // The base models are in GROK_3_MINI_MODELS expect(GROK_3_MINI_MODELS).toContain('grok-3-mini-beta'); // Verify aliases are properly linked const miniModel = XAI_CHAT_MODELS.find((m) => m.id === 'grok-3-mini-beta'); expect(miniModel?.aliases).toBeDefined(); expect(miniModel?.aliases?.length).toBeGreaterThan(0); }); }); describe('Cost calculation', () => { it('calculates costs correctly for all model types', () => { // Test Grok-2 - calculateXAICost(modelName, config, promptTokens, completionTokens) const grok2Cost = calculateXAICost('grok-2-1212', {}, 600, 400); expect(grok2Cost).toBeDefined(); expect(typeof grok2Cost).toBe('number'); // Test Grok-4 const grok4Cost = calculateXAICost('grok-4-0709', {}, 600, 400); expect(grok4Cost).toBeDefined(); // Test Grok-3 mini (reasoning model) const grok3MiniCost = calculateXAICost('grok-3-mini-beta', {}, 600, 400); expect(grok3MiniCost).toBeDefined(); }); it('handles model aliases correctly', () => { // grok-4 is an alias for grok-4-0709 const aliasCost = calculateXAICost('grok-4', {}, 600, 400); expect(aliasCost).toBeDefined(); // grok-3-mini is an alias for grok-3-mini-beta const miniAliasCost = calculateXAICost('grok-3-mini', {}, 600, 400); expect(miniAliasCost).toBeDefined(); }); it('applies the cache-read rate to cached prompt tokens', () => { // The model table defines cache_read for grok-4, but it was never applied. const full = calculateXAICost('grok-4-0709', {}, 1000, 500); const discounted = calculateXAICost('grok-4-0709', {}, 1000, 500, 0, 800); expect(full).toBeDefined(); expect(discounted as number).toBeLessThan(full as number); }); it('bills the cached prompt subset at the cache-read rate (deterministic)', () => { // input $3/M, output $15/M, cache-read $0.75/M; 1000 prompt (800 cached), 500 output. const rates = { inputCost: 3e-6, outputCost: 15e-6, cacheReadCost: 0.75e-6 }; // (200 uncached * 3e-6) + (800 cached * 0.75e-6) + (500 * 15e-6) = 0.0087 expect(calculateXAICost('grok-4-0709', rates, 1000, 500, 0, 800)).toBeCloseTo(0.0087, 10); // Without any cached tokens: (1000 * 3e-6) + (500 * 15e-6) = 0.0105 expect(calculateXAICost('grok-4-0709', rates, 1000, 500)).toBeCloseTo(0.0105, 10); // A proxy may make cache hits free; an explicit zero must not fall back to model pricing. expect( calculateXAICost('grok-4-0709', { ...rates, cacheReadCost: 0 }, 1000, 500, 0, 800), ).toBeCloseTo(0.0081, 10); }); it('clamps cached prompt tokens to valid usage boundaries', () => { const rates = { inputCost: 3e-6, outputCost: 15e-6, cacheReadCost: 0.75e-6 }; // A full cache hit bills every prompt token at the reduced rate. expect(calculateXAICost('grok-4-0709', rates, 1000, 500, 0, 1000)).toBeCloseTo(0.00825, 10); // Inconsistent provider usage cannot discount more tokens than the prompt contains. expect(calculateXAICost('grok-4-0709', rates, 1000, 500, 0, 2000)).toBeCloseTo(0.00825, 10); // Invalid negative usage falls back to the undiscounted input cost. expect(calculateXAICost('grok-4-0709', rates, 1000, 500, 0, -1)).toBeCloseTo(0.0105, 10); expect(calculateXAICost('grok-4-0709', rates, 1000, 500, 0, Number.NaN)).toBeCloseTo( 0.0105, 10, ); expect(calculateXAICost('grok-4-0709', rates, 1000, 500, 0, Infinity)).toBeCloseTo( 0.0105, 10, ); }); it('falls back to full input pricing when the model has no cache-read rate', () => { // grok-2-1212 has input: 2.0/1e6, output: 10.0/1e6, and no cache_read rate. // Cached tokens reported by the provider must not produce a NaN cost or a // phantom discount; they bill at the full input rate exactly as if no cache // tokens were reported. const withCache = calculateXAICost('grok-2-1212', {}, 1000, 500, 0, 800); const withoutCache = calculateXAICost('grok-2-1212', {}, 1000, 500); // (2.0/1e6 * 1000) + (10.0/1e6 * 500) = 0.002 + 0.005 = 0.007 expect(withCache).toBeCloseTo(0.007, 10); expect(withCache).toBe(withoutCache); }); it('uses Grok 4.3 fallback pricing for redirected legacy chat slugs', () => { for (const modelName of [ 'grok-4-1-fast-reasoning', 'grok-4-fast-non-reasoning', 'grok-4', 'grok-3', // The grok-3 family collapses every -beta and -fast variant into the // same id on xAI's catalog, so all of them must redirect together. 'grok-3-beta', 'grok-3-fast', 'grok-3-fast-beta', 'grok-3-fast-latest', ]) { expect(calculateXAICost(modelName, {}, 1_000_000, 1_000_000)).toBeCloseTo(3.75, 10); } }); it('bills grok-build-0.1 and its grok-code-fast aliases at its own pricing', () => { // xAI's docs list grok-code-fast-1 / grok-code-fast / grok-code-fast-1-0825 as // aliases of grok-build-0.1 ($1.00 input / $2.00 output per 1M tokens), so they // must NOT redirect to grok-4.3 pricing. for (const modelName of [ 'grok-build-0.1', 'grok-code-fast-1', 'grok-code-fast', 'grok-code-fast-1-0825', ]) { expect(calculateXAICost(modelName, {}, 100_000, 1_000_000)).toBeCloseTo(2.1, 10); } }); it('switches grok-build-0.1 and its aliases to higher-context pricing above 200k tokens', () => { for (const modelName of [ 'grok-build-0.1', 'grok-code-fast-1', 'grok-code-fast', 'grok-code-fast-1-0825', ]) { // Exactly at the threshold stays on the standard tier (exclusive >). expect(calculateXAICost(modelName, {}, 200_000, 1_000)).toBeCloseTo( (200_000 * 1 + 1_000 * 2) / 1e6, 10, ); // One token over switches the whole request to the higher tier. expect(calculateXAICost(modelName, {}, 200_001, 1_000)).toBeCloseTo( (200_001 * 2 + 1_000 * 4) / 1e6, 10, ); } }); it('uses higher-context cache-read pricing for grok-build-0.1 cached prompt tokens', () => { expect(calculateXAICost('grok-build-0.1', {}, 200_001, 1_000, 0, 100_000)).toBeCloseTo( (100_001 * 2 + 100_000 * 0.4 + 1_000 * 4) / 1e6, 10, ); }); it('returns undefined for invalid inputs', () => { // Unknown model expect(calculateXAICost('invalid-model', {}, 600, 400)).toBe(undefined); // Missing token counts expect(calculateXAICost('grok-2-1212', {})).toBe(undefined); expect(calculateXAICost('grok-2-1212', {}, 0, 0)).toBe(undefined); }); it('calculates cost based on model pricing', () => { // grok-2-1212 has input: 2.0/1e6 and output: 10.0/1e6 const cost = calculateXAICost('grok-2-1212', {}, 1000000, 1000000); expect(cost).toBeDefined(); // Expected: (2.0/1e6 * 1000000) + (10.0/1e6 * 1000000) = 2.0 + 10.0 = 12.0 expect(cost).toBeCloseTo(12.0, 2); }); it('uses separate custom input and output costs from config', () => { const cost = calculateXAICost( 'grok-2-1212', { inputCost: 0.001, outputCost: 0.003 }, 1000, 500, ); expect(cost).toBe(2.5); }); it('prefers separate custom costs over custom cost', () => { const cost = calculateXAICost( 'grok-2-1212', { cost: 0.02, inputCost: 0.001, outputCost: 0.003 }, 1000, 500, ); expect(cost).toBe(2.5); }); it('does not mix model cache pricing with custom input cost overrides', () => { expect(calculateXAICost('grok-4-0709', { cost: 0.001 }, 1000, 500, 0, 800)).toBe(1.5); expect( calculateXAICost('grok-4-0709', { inputCost: 0.001, outputCost: 0.003 }, 1000, 500, 0, 800), ).toBe(2.5); }); it('does not double-count reasoning tokens already included in completion tokens', () => { // grok-3-mini-beta: input $0.30/M, output $0.50/M. const baseline = calculateXAICost('grok-3-mini-beta', {}, 500, 500); // 500 input @ 0.30/M + 500 output @ 0.50/M = 0.00015 + 0.00025 = 0.0004 expect(baseline).toBeCloseTo(0.0004, 10); // completion_tokens already INCLUDES reasoning_tokens (the OpenAI // convention xAI follows), so passing reasoning separately must not add // to the bill — the cost is identical to the baseline. const withReasoning = calculateXAICost('grok-3-mini-beta', {}, 500, 500, 200); expect(withReasoning).toBeCloseTo(0.0004, 10); }); it('bills a reasoning-only response (no other completion tokens)', () => { // grok-3-mini-beta: input $0.30/M, output $0.50/M. A turn that reports // zero completion tokens but non-zero reasoning still has a real cost: we // fall back to billing the reasoning tokens at the output rate. const cost = calculateXAICost('grok-3-mini-beta', {}, 500, 0, 200); // 500 input @ 0.30/M + 200 reasoning @ 0.50/M = 0.00015 + 0.0001 expect(cost).toBeCloseTo(0.00025, 10); }); it('bills only completion tokens (reasoning included) when input tokens are zero', () => { const cost = calculateXAICost('grok-3-mini-beta', {}, 0, 200, 50); // 0 input @ 0.30/M + 200 completion @ 0.50/M (the 50 reasoning tokens are // already part of the 200 completion tokens, not added on top). expect(cost).toBeCloseTo(0.0001, 10); }); }); describe('Model constants and configuration', () => { it('defines correct Grok-3 mini models', () => { expect(GROK_3_MINI_MODELS).toContain('grok-3-mini-beta'); expect(GROK_3_MINI_MODELS).toContain('grok-3-mini-fast-beta'); }); it('defines model costs correctly', () => { // Check that XAI_CHAT_MODELS has cost information const modelsWithCosts = XAI_CHAT_MODELS.filter((m) => m.cost); expect(modelsWithCosts.length).toBeGreaterThan(0); // Verify cost structure modelsWithCosts.forEach((model) => { expect(model.cost).toBeDefined(); expect(model.cost?.input).toBeDefined(); expect(model.cost?.output).toBeDefined(); }); }); it('includes all required model properties', () => { XAI_CHAT_MODELS.forEach((model) => { expect(model.id).toBeDefined(); expect(typeof model.id).toBe('string'); }); }); it('has correct aliases for models', () => { // grok-4 should have grok-4-latest alias const grok4 = XAI_CHAT_MODELS.find((m) => m.id === 'grok-4-0709'); expect(grok4?.aliases).toContain('grok-4'); expect(grok4?.aliases).toContain('grok-4-latest'); // grok-3-beta should have aliases const _grok3 = XAI_CHAT_MODELS.find((m) => m.id === 'grok-3-beta'); const modelWithAliases = XAI_CHAT_MODELS.find((m) => m.aliases?.includes('grok-3')); expect(modelWithAliases).toBeDefined(); expect(modelWithAliases?.aliases).toEqual(['grok-3', 'grok-3-latest']); }); }); describe('callApi error handling', () => { // These tests verify XAIProvider's error handling behavior // Note: When fetchWithCache throws, the parent class (OpenAiChatCompletionProvider) // catches it first and returns { error: 'API call error: ...' } // XAIProvider then enhances specific error patterns with xAI-specific messages it('should pass through errors from parent class', async () => { // When fetchWithCache throws, parent class catches and returns error object mockFetchWithCache.mockRejectedValueOnce(new Error('Network timeout')); const provider = createXAIProvider('xai:grok-4', { config: { apiKey: 'test-key', } as any, }); const result = await provider.callApi('test prompt'); expect(result.error).toBeDefined(); // Error comes through parent class first expect(result.error).toContain('API call error:'); }); it('should enhance 502 Bad Gateway errors in response.error', async () => { // When the parent returns an error containing '502 Bad Gateway', // XAIProvider enhances it with xAI-specific messaging mockFetchWithCache.mockResolvedValueOnce({ data: { error: { message: 'Bad Gateway' } }, cached: false, status: 502, statusText: 'Bad Gateway', }); const provider = createXAIProvider('xai:grok-4', { config: { apiKey: 'test-key', } as any, }); const result = await provider.callApi('test prompt'); expect(result.error).toBeDefined(); // XAIProvider enhances errors containing '502 Bad Gateway' expect(result.error).toContain('x.ai API error:'); expect(result.error).toContain('XAI_API_KEY'); }); it('should handle authentication errors', async () => { // Mock fetchWithCache to return an authentication error mockFetchWithCache.mockResolvedValueOnce({ data: { error: { message: 'Invalid API key' } }, cached: false, status: 401, statusText: 'Unauthorized', }); const provider = createXAIProvider('xai:grok-4', { config: { apiKey: 'test-key', } as any, }); const result = await provider.callApi('test prompt'); expect(result.error).toBeDefined(); expect(result.error).toContain('API error: 401'); }); it('should pass through successful responses', async () => { const successResponse = { data: { choices: [{ message: { content: 'Test response' } }], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }, }, cached: false, status: 200, statusText: 'OK', }; mockFetchWithCache.mockResolvedValueOnce(successResponse); const provider = createXAIProvider('xai:grok-4', { config: { apiKey: 'test-key', } as any, }); const result = await provider.callApi('test prompt'); expect(result.output).toBe('Test response'); expect(result.error).toBeUndefined(); }); it('should include token usage in response', async () => { mockFetchWithCache.mockResolvedValueOnce({ data: { choices: [{ message: { content: 'Response with tokens' } }], usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 }, }, cached: false, status: 200, statusText: 'OK', }); const provider = createXAIProvider('xai:grok-4', { config: { apiKey: 'test-key', } as any, }); const result = await provider.callApi('test prompt'); expect(result.output).toBe('Response with tokens'); expect(result.tokenUsage).toBeDefined(); expect(result.tokenUsage?.prompt).toBe(100); expect(result.tokenUsage?.completion).toBe(50); expect(result.tokenUsage?.total).toBe(150); }); it('should calculate cost for successful responses', async () => { mockFetchWithCache.mockResolvedValueOnce({ data: { choices: [{ message: { content: 'Test response' } }], usage: { prompt_tokens: 1000, completion_tokens: 500, total_tokens: 1500 }, }, cached: false, status: 200, statusText: 'OK', }); const provider = createXAIProvider('xai:grok-4', { config: { apiKey: 'test-key', } as any, }); const result = await provider.callApi('test prompt'); expect(result.output).toBe('Test response'); expect(result.cost).toBeDefined(); expect(typeof result.cost).toBe('number'); }); it('should apply cache-read pricing from normalized token usage', async () => { mockFetchWithCache.mockResolvedValueOnce({ data: { choices: [{ message: { content: 'Cached response' } }], usage: { prompt_tokens: 1000, completion_tokens: 500, total_tokens: 1500, prompt_tokens_details: { cached_tokens: 800 }, }, }, cached: false, status: 200, statusText: 'OK', }); const provider = createXAIProvider('xai:grok-4-0709', { config: { config: { apiKey: 'test-key', inputCost: 3e-6, outputCost: 15e-6, cacheReadCost: 0.75e-6, }, }, }); const result = await provider.callApi('test prompt'); expect(result.tokenUsage?.completionDetails?.cacheReadInputTokens).toBe(800); expect(result.cost).toBeCloseTo(0.0087, 10); }); it('should leave cost undefined when usage is missing', async () => { mockFetchWithCache.mockResolvedValueOnce({ data: { choices: [{ message: { content: 'Response without usage' } }], }, cached: false, status: 200, statusText: 'OK', }); const provider = createXAIProvider('xai:grok-4-0709', { config: { apiKey: 'test-key' } as any, }); const result = await provider.callApi('test prompt'); expect(result.output).toBe('Response without usage'); expect(result.cost).toBeUndefined(); }); it('does not double-count reasoning tokens already included in completion tokens', async () => { mockFetchWithCache.mockResolvedValueOnce({ data: { choices: [{ message: { content: 'reasoned answer' } }], usage: { prompt_tokens: 500, // completion_tokens already includes the 200 reasoning tokens // (prompt 500 + completion 500 = total 1000 confirms reasoning is a // subset of completion, not a separate addend). completion_tokens: 500, total_tokens: 1000, completion_tokens_details: { reasoning_tokens: 200 }, }, }, cached: false, status: 200, statusText: 'OK', }); const provider = createXAIProvider('xai:grok-3-mini-beta', { config: { apiKey: 'test-key' } as any, }); const result = await provider.callApi('test prompt'); // grok-3-mini-beta: input $0.30/M, output $0.50/M. Reasoning tokens are // already part of the 500 completion tokens, so bill 500 output tokens: // 500 input @ 0.30/M + 500 output @ 0.50/M = 0.00015 + 0.00025 = 0.0004. expect(result.tokenUsage?.completionDetails?.reasoning).toBe(200); expect(result.cost).toBeCloseTo(0.0004, 10); }); }); });