diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..cafe685 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=true diff --git a/index.ts b/index.ts index 771657e..7486761 100644 --- a/index.ts +++ b/index.ts @@ -16,11 +16,6 @@ const stackable: Stackable = async function (fastify, opts) { await fastify.register(fastifyUser as any, config.auth) await fastify.register(authPlugin, opts) - await fastify.register(warpPlugin, opts) // needs to be registered here for fastify.ai to be decorated - - await fastify.register(rateLimitPlugin, opts) - await fastify.register(apiPlugin, opts) - if (config.showAiWarpHomepage !== undefined && config.showAiWarpHomepage) { await fastify.register(fastifyStatic, { root: join(import.meta.dirname, 'static') @@ -28,6 +23,12 @@ const stackable: Stackable = async function (fastify, opts) { } await fastify.register(platformaticService, opts) + + await fastify.register(warpPlugin, opts) // needs to be registered here for fastify.ai to be decorated + + await fastify.register(rateLimitPlugin, opts) + await fastify.register(apiPlugin, opts) + } stackable.configType = 'ai-warp-app' diff --git a/package.json b/package.json index e61c2c4..2ce79fa 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "lint-md": "markdownlint-cli2 .", "lint-md:fix": "markdownlint-cli2 --fix .", "test": "npm run test:unit && npm run test:e2e && npm run test:types", - "test:unit": "node --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout --import=tsx ./tests/unit/index.ts", - "test:e2e": "node --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout --import=tsx ./tests/e2e/index.ts", + "test:unit": "node --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout --import=tsx --test-concurrency=1 ./tests/unit/*", + "test:e2e": "node --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout --import=tsx --test-concurrency=1 ./tests/e2e/*", "test:types": "tsd" }, "engines": { diff --git a/plugins/api.ts b/plugins/api.ts index 2de4b91..54080dd 100644 --- a/plugins/api.ts +++ b/plugins/api.ts @@ -23,7 +23,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { response: Type.String() }), default: Type.Object({ - code: Type.String(), + code: Type.Optional(Type.String()), message: Type.String() }) } diff --git a/tests/e2e/api.test.ts b/tests/e2e/api.test.ts index d7cfd7a..3844d95 100644 --- a/tests/e2e/api.test.ts +++ b/tests/e2e/api.test.ts @@ -1,4 +1,5 @@ -/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript +* eslint/no-floating-promises */ import { before, after, describe, it } from 'node:test' import assert from 'node:assert' import { FastifyInstance } from 'fastify' @@ -8,9 +9,12 @@ import { buildAiWarpApp } from '../utils/stackable.js' import { AZURE_DEPLOYMENT_NAME, AZURE_MOCK_HOST } from '../utils/mocks/azure.js' import { MOCK_CONTENT_RESPONSE, buildExpectedStreamBodyString } from '../utils/mocks/base.js' import { OLLAMA_MOCK_HOST } from '../utils/mocks/ollama.js' +import { mockAllProviders } from '../utils/mocks/index.js' +mockAllProviders() const expectedStreamBody = buildExpectedStreamBodyString() + interface Provider { name: string config: AiWarpConfig['aiProvider'] @@ -108,6 +112,7 @@ for (const { name, config } of providers) { prompt: 'asd' }) }) + assert.strictEqual(res.headers.get('content-type'), 'text/event-stream') assert.strictEqual(chunkCallbackCalled, true) @@ -170,3 +175,59 @@ it('calls the preResponseCallback', async () => { await app.close() }) + +it('provides all paths in OpenAPI', async () => { + const [app, port] = await buildAiWarpApp({ + aiProvider: { + openai: { + model: 'gpt-3.5-turbo', + apiKey: '' + } + } + }) + + await app.start() + + const res = await fetch(`http://localhost:${port}/documentation/json`) + const body = await res.json() + + assert.deepStrictEqual(Object.keys(body.paths), [ + '/api/v1/prompt', + '/api/v1/stream' + ]) + + await app.close() +}) + +it('prompt with wrong JSON', async () => { + const [app, port] = await buildAiWarpApp({ + aiProvider: { + openai: { + model: 'gpt-3.5-turbo', + apiKey: '' + } + } + }) + + await app.start() + + const res = await fetch(`http://localhost:${port}/api/v1/prompt`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + prompt: 'asd' + }).slice(0, 10) + }) + + assert.strictEqual(res.status, 400) + + const body = await res.json() + + assert.deepStrictEqual(body, { + message: 'Unexpected end of JSON input' + }) + + await app.close() +}) diff --git a/tests/e2e/auth.test.ts b/tests/e2e/auth.test.ts index e099bd3..0970c59 100644 --- a/tests/e2e/auth.test.ts +++ b/tests/e2e/auth.test.ts @@ -4,6 +4,8 @@ import assert from 'node:assert' import { buildAiWarpApp } from '../utils/stackable.js' import { AiWarpConfig } from '../../config.js' import { authConfig, createToken } from '../utils/auth.js' +import { mockAllProviders } from '../utils/mocks/index.js' +mockAllProviders() const aiProvider: AiWarpConfig['aiProvider'] = { openai: { diff --git a/tests/e2e/index.ts b/tests/e2e/index.ts deleted file mode 100644 index b675db9..0000000 --- a/tests/e2e/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import './api.test' -import './rate-limiting.test' -import './auth.test' -import { mockAllProviders } from '../utils/mocks/index.js' - -mockAllProviders() diff --git a/tests/e2e/rate-limiting.test.ts b/tests/e2e/rate-limiting.test.ts index da0408e..5aa007a 100644 --- a/tests/e2e/rate-limiting.test.ts +++ b/tests/e2e/rate-limiting.test.ts @@ -5,6 +5,8 @@ import fastifyPlugin from 'fastify-plugin' import { AiWarpConfig } from '../../config.js' import { buildAiWarpApp } from '../utils/stackable.js' import { authConfig, createToken } from '../utils/auth.js' +import { mockAllProviders } from '../utils/mocks/index.js' +mockAllProviders() const aiProvider: AiWarpConfig['aiProvider'] = { openai: { @@ -28,7 +30,15 @@ it('calls ai.rateLimiting.max callback', async () => { await app.start() - const res = await fetch(`http://localhost:${port}`) + const res = await fetch(`http://localhost:${port}/api/v1/prompt`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + prompt: 'asd' + }) + }) assert.strictEqual(callbackCalled, true) assert.strictEqual(res.headers.get('x-ratelimit-limit'), `${expectedMax}`) } finally { @@ -50,7 +60,15 @@ it('calls ai.rateLimiting.allowList callback', async () => { await app.start() - await fetch(`http://localhost:${port}`) + await fetch(`http://localhost:${port}/api/v1/prompt`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + prompt: 'asd' + }) + }) assert.strictEqual(callbackCalled, true) } finally { await app.close() @@ -76,13 +94,21 @@ it('calls ai.rateLimiting.onBanReach callback', async () => { app.ai.rateLimiting.errorResponseBuilder = () => { errorResponseBuilderCalled = true - return { error: 'rate limited' } + return { message: 'rate limited' } } })) await app.start() - await fetch(`http://localhost:${port}`) + await fetch(`http://localhost:${port}/api/v1/prompt`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + prompt: 'asd' + }) + }) assert.strictEqual(onBanReachCalled, true) assert.strictEqual(errorResponseBuilderCalled, true) } finally { @@ -104,7 +130,15 @@ it('calls ai.rateLimiting.keyGenerator callback', async () => { await app.start() - await fetch(`http://localhost:${port}`) + await fetch(`http://localhost:${port}/api/v1/prompt`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + prompt: 'asd' + }) + }) assert.strictEqual(callbackCalled, true) } finally { await app.close() @@ -120,13 +154,21 @@ it('calls ai.rateLimiting.errorResponseBuilder callback', async () => { app.ai.rateLimiting.max = () => 0 app.ai.rateLimiting.errorResponseBuilder = () => { callbackCalled = true - return { error: 'rate limited' } + return { message: 'rate limited' } } })) await app.start() - await fetch(`http://localhost:${port}`) + await fetch(`http://localhost:${port}/api/v1/prompt`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + prompt: 'asd' + }) + }) assert.strictEqual(callbackCalled, true) } finally { await app.close() @@ -156,17 +198,27 @@ it('uses the max for a specific claim', async () => { try { await app.start() - let res = await fetch(`http://localhost:${port}`, { + let res = await fetch(`http://localhost:${port}/api/v1/prompt`, { + method: 'POST', headers: { - Authorization: `Bearer ${createToken({ rateLimitMax: '10' })}` - } + Authorization: `Bearer ${createToken({ rateLimitMax: '10' })}`, + 'content-type': 'application/json' + }, + body: JSON.stringify({ + prompt: 'asd' + }) }) assert.strictEqual(res.headers.get('x-ratelimit-limit'), '10') - res = await fetch(`http://localhost:${port}`, { + res = await fetch(`http://localhost:${port}/api/v1/prompt`, { + method: 'POST', headers: { - Authorization: `Bearer ${createToken({ rateLimitMax: '100' })}` - } + Authorization: `Bearer ${createToken({ rateLimitMax: '100' })}`, + 'content-type': 'application/json' + }, + body: JSON.stringify({ + prompt: 'asd' + }) }) assert.strictEqual(res.headers.get('x-ratelimit-limit'), '100') } finally { diff --git a/tests/unit/ai-providers.test.ts b/tests/unit/ai-providers.test.ts index 3f59c73..48d0d67 100644 --- a/tests/unit/ai-providers.test.ts +++ b/tests/unit/ai-providers.test.ts @@ -10,6 +10,8 @@ import { MOCK_CONTENT_RESPONSE, buildExpectedStreamBodyString } from '../utils/m import { OLLAMA_MOCK_HOST } from '../utils/mocks/ollama.js' import { AZURE_DEPLOYMENT_NAME, AZURE_MOCK_HOST } from '../utils/mocks/azure.js' import { mockLlama2 } from '../utils/mocks/llama2.js' +import { mockAllProviders } from '../utils/mocks/index.js' +mockAllProviders() const expectedStreamBody = buildExpectedStreamBodyString() diff --git a/tests/unit/generator.test.ts b/tests/unit/generator.test.ts index ee18121..ad39487 100644 --- a/tests/unit/generator.test.ts +++ b/tests/unit/generator.test.ts @@ -7,6 +7,8 @@ import { join } from 'node:path' import AiWarpGenerator from '../../lib/generator.js' import { generateGlobalTypesFile } from '../../lib/templates/types.js' import { generatePluginWithTypesSupport } from '@platformatic/generators/lib/create-plugin.js' +import { mockAllProviders } from '../utils/mocks/index.js' +mockAllProviders() const tempDirBase = join(import.meta.dirname, 'tmp') diff --git a/tests/unit/index.ts b/tests/unit/index.ts deleted file mode 100644 index 7f8249b..0000000 --- a/tests/unit/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import './generator.test' -import './ai-providers.test' -import { mockAllProviders } from '../utils/mocks/index.js' - -mockAllProviders() diff --git a/tests/utils/stackable.ts b/tests/utils/stackable.ts index 7c87fd9..6466fae 100644 --- a/tests/utils/stackable.ts +++ b/tests/utils/stackable.ts @@ -21,13 +21,14 @@ export async function buildAiWarpApp (config: AiWarpConfig): Promise<[FastifyIns server: { port, forceCloseConnections: true, - healthCheck: { - enabled: false - }, + healthCheck: false, logger: { level: 'silent' } }, + service: { + openapi: true + }, ...config }, stackable)