import { describe, test, expect, vi, beforeEach } from 'vitest' import { Cognitive } from '../src/client' import { getTestClient } from './client' import MODELS from './models.json' import { RemoteModelProvider } from '../src/models' import { GenerateContentOutput } from '../src/schemas.gen' const RandomResponse = { output: { botpress: { cost: 123 }, choices: [{ role: 'assistant', content: 'This is the LLM response', stopReason: 'stop', index: 1 }], id: '123456', model: '', provider: '', usage: { inputCost: 1, inputTokens: 2, outputCost: 3, outputTokens: 4 }, } satisfies GenerateContentOutput, meta: {}, } as const // Simple mock for the provider class MockProvider extends RemoteModelProvider { fetchModelPreferences = vi.fn().mockResolvedValue(null) fetchInstalledModels = vi.fn().mockResolvedValue(MODELS) saveModelPreferences = vi.fn().mockResolvedValue(void 0) } class TestClient { callAction = vi.fn().mockImplementation(() => { if (this.axiosInstance.defaults?.signal?.aborted) { throw this.axiosInstance.defaults?.signal.reason ?? 'Aborted' } return Promise.resolve(RandomResponse) }) getBot = vi.fn() getFile = vi.fn() axiosInstance = { defaults: { signal: new AbortController().signal }, } config = { headers: { 'x-bot-id': 'test' } } clone = () => this } describe('constructor', () => { test('valid client', () => { // Just check that no error is thrown const provider = new MockProvider(getTestClient()) expect(() => new Cognitive({ client: getTestClient(), provider })).not.toThrow() }) }) describe('client', () => { let bp: TestClient let client: Cognitive let provider: MockProvider beforeEach(() => { vi.clearAllMocks() bp = new TestClient() provider = new MockProvider(bp) client = new Cognitive({ client: bp, provider }) }) describe('predict (request)', () => { test('fetches models when preferences are not available and saves the preferences', async () => { await client.generateContent({ messages: [], model: 'best' }) expect(provider.fetchModelPreferences).toHaveBeenCalled() expect(provider.fetchInstalledModels).toHaveBeenCalled() expect(provider.saveModelPreferences).toHaveBeenCalled() }) test('fetches model preferences the first time generateContent is called', async () => { await client.generateContent({ messages: [], model: 'fast' }) // fetchInstalledModels is called because fetchModelPreferences returned null expect(provider.fetchInstalledModels).toHaveBeenCalledTimes(1) // A second call won't fetch again if preferences are cached await client.generateContent({ messages: [], model: 'fast' }) expect(provider.fetchInstalledModels).toHaveBeenCalledTimes(1) }) }) describe('predict (fallback)', () => { test('when model is unavailable, registers the downtime, saves it, and selects another model', async () => { client = new Cognitive({ client: bp, provider }) bp.callAction.mockRejectedValueOnce({ isApiError: true, code: 400, id: '123', type: 'Runtime', metadata: { subtype: 'UPSTREAM_PROVIDER_FAILED' }, }) provider.fetchModelPreferences.mockResolvedValue({ best: ['a:a', 'b:b'], }) // First generate call triggers fallback await client.generateContent({ messages: [], model: 'a:a' }) expect(bp.callAction).toHaveBeenCalledTimes(2) expect(provider.saveModelPreferences).toHaveBeenCalledOnce() expect(provider.saveModelPreferences.mock.calls[0]?.[0].best).toMatchObject(['a:a', 'b:b']) expect(provider.saveModelPreferences.mock.calls[0]?.[0].downtimes[0].ref).toBe('a:a') }) }) describe('predict (abort)', () => { test('abort request', async () => { const ac = new AbortController() ac.abort('Manual abort') await expect(client.generateContent({ messages: [], signal: ac.signal })).rejects.toMatch('Manual abort') }) }) describe('predict (response)', () => { test('request cost and metrics are returned', async () => { const resp = await client.generateContent({ messages: [] }) expect(resp.meta.cost.input).toBe(1) expect(resp.meta.cost.output).toBe(3) expect(resp.meta.tokens.input).toBe(2) expect(resp.meta.tokens.output).toBe(4) expect(resp.output.choices[0]?.content).toBe('This is the LLM response') }) }) describe('getModelDetails', () => { test('fetches model details', async () => { const details = await client.getModelDetails('best') expect(details).toMatchInlineSnapshot(` { "description": "GPT-4o (“o” for “omni”) is OpenAI's most advanced model. It is multimodal (accepting text or image inputs and outputting text), and it has the same high intelligence as GPT-4 Turbo but is cheaper and more efficient.", "id": "gpt-4o-2024-11-20", "input": { "costPer1MTokens": 2.5, "maxTokens": 128000, }, "integration": "openai", "name": "GPT-4o (November 2024)", "output": { "costPer1MTokens": 10, "maxTokens": 16384, }, "ref": "openai:gpt-4o-2024-11-20", "tags": [ "recommended", "vision", "general-purpose", "coding", "agents", "function-calling", ], } `) }) }) }) test('isCognitiveClient', () => { const client = getTestClient() expect(Cognitive.isCognitiveClient(client)).toBe(false) expect(Cognitive.isCognitiveClient(new Cognitive({ client, provider: new MockProvider(client) }))).toBe(true) })