diff --git a/packages/corsair/core/constants.ts b/packages/corsair/core/constants.ts index 93ce1126d..008bdc191 100644 --- a/packages/corsair/core/constants.ts +++ b/packages/corsair/core/constants.ts @@ -152,6 +152,7 @@ export const BaseProviders = [ 'spotify', 'strava', 'stripe', + 'studiobyai21labs', 'supabase', 'tally', 'tavily', @@ -319,6 +320,7 @@ export const ProviderDisplayNames = { spotify: 'Spotify', strava: 'Strava', stripe: 'Stripe', + studiobyai21labs: 'StudioByAI21Labs', supabase: 'Supabase', tally: 'Tally', tavily: 'Tavily', @@ -493,6 +495,7 @@ export type AllProviders = | 'spotify' | 'strava' | 'stripe' + | 'studiobyai21labs' | 'supabase' | 'tally' | 'tavily' diff --git a/packages/studiobyai21labs/api.test.ts b/packages/studiobyai21labs/api.test.ts new file mode 100644 index 000000000..a97fd865d --- /dev/null +++ b/packages/studiobyai21labs/api.test.ts @@ -0,0 +1,492 @@ +import { AuthMissingError, logEventFromContext } from 'corsair/core'; +import { ApiError, request } from 'corsair/http'; +import { + makeStudioByAI21LabsRequest, + STUDIOBYAI21LABS_API_BASE, +} from './client'; +import { Chat, Library, Maestro } from './endpoints'; +import { + StudioByAI21LabsEndpointInputSchemas, + StudioByAI21LabsEndpointOutputSchemas, +} from './endpoints/types'; +import { errorHandlers } from './error-handlers'; +import type { StudioByAI21LabsContext } from './index'; +import { studioByAI21LabsEndpointSchemas, studiobyai21labs } from './index'; + +jest.mock('corsair/http', () => { + const original = jest.requireActual('corsair/http'); + return { + ...original, + request: jest.fn(), + }; +}); + +jest.mock('corsair/core', () => { + const original = jest.requireActual('corsair/core'); + return { + ...original, + logEventFromContext: jest.fn().mockResolvedValue(undefined), + }; +}); + +const mockRequest = request as jest.Mock; +const mockLogEvent = logEventFromContext as jest.Mock; + +function testCtx(key = 'test-key'): StudioByAI21LabsContext { + return { + key, + $getAccountId: async () => 'test-account-id', + } as unknown as StudioByAI21LabsContext; +} + +function endpointPaths(tree: Record, prefix = ''): string[] { + return Object.entries(tree).flatMap(([key, value]) => { + const path = prefix ? `${prefix}.${key}` : key; + if (typeof value === 'function') return [path]; + if (value && typeof value === 'object') { + return endpointPaths(value as Record, path); + } + return []; + }); +} + +describe('StudioByAI21Labs plugin shape', () => { + it('registers the official AI21 Studio operations and no webhooks', () => { + const plugin = studiobyai21labs(); + const paths = endpointPaths( + plugin.endpoints as Record, + ).sort(); + + expect(paths).toEqual([ + 'chat.completions', + 'library.delete', + 'library.download', + 'library.get', + 'library.list', + 'library.update', + 'library.upload', + 'maestro.createRun', + 'maestro.retrieveRun', + ]); + expect(Object.keys(plugin.endpointMeta ?? {}).sort()).toEqual(paths); + expect(Object.keys(studioByAI21LabsEndpointSchemas).sort()).toEqual(paths); + expect(plugin.webhooks).toEqual({}); + expect(plugin.pluginWebhookMatcher?.({ headers: {}, body: '' })).toBe( + false, + ); + expect(plugin.options?.authType).toBe('api_key'); + expect(plugin.schema?.entities).toEqual({}); + }); +}); + +describe('StudioByAI21Labs schemas', () => { + it('parses chat completions input and response', () => { + const input = + StudioByAI21LabsEndpointInputSchemas.chatCompletions.safeParse({ + model: 'jamba-large', + messages: [{ role: 'user', content: 'Hello' }], + max_tokens: 1024, + }); + expect(input.success).toBe(true); + + const output = + StudioByAI21LabsEndpointOutputSchemas.chatCompletions.safeParse({ + id: 'cmpl-1', + choices: [ + { + index: 0, + message: { role: 'assistant', content: 'Hi' }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 4, + completion_tokens: 1, + total_tokens: 5, + }, + }); + expect(output.success).toBe(true); + }); + + it('rejects chat completions without messages', () => { + const invalid = + StudioByAI21LabsEndpointInputSchemas.chatCompletions.safeParse({ + model: 'jamba-large', + }); + expect(invalid.success).toBe(false); + }); + + it('rejects chat completions stream field', () => { + const invalid = + StudioByAI21LabsEndpointInputSchemas.chatCompletions.safeParse({ + model: 'jamba-large', + messages: [{ role: 'user', content: 'Hello' }], + stream: true, + }); + expect(invalid.success).toBe(false); + }); + + it('parses library list, file, and download schemas', () => { + expect( + StudioByAI21LabsEndpointInputSchemas.listLibraryFiles.safeParse({ + status: 'PROCESSED', + limit: 10, + }).success, + ).toBe(true); + expect( + StudioByAI21LabsEndpointOutputSchemas.listLibraryFiles.safeParse([ + { + id: 'file-1', + name: 'notes.txt', + size: 12, + created_at: '2025-10-20T14:23:11Z', + labels: ['invoices'], + }, + ]).success, + ).toBe(true); + expect( + StudioByAI21LabsEndpointInputSchemas.uploadWorkspaceFile.safeParse({ + publicUrl: 'https://example.com/file.pdf', + path: 'docs/file.pdf', + labels: ['docs'], + }).success, + ).toBe(true); + expect( + StudioByAI21LabsEndpointInputSchemas.uploadWorkspaceFile.safeParse({}) + .success, + ).toBe(false); + expect( + StudioByAI21LabsEndpointOutputSchemas.getFileDownloadLink.safeParse( + 'https://storage.ai21.com/files/file_123abc/download?token=xyz', + ).success, + ).toBe(true); + }); + + it('parses maestro create and retrieve schemas', () => { + expect( + StudioByAI21LabsEndpointInputSchemas.createMaestroRun.safeParse({ + input: [ + { + role: 'user', + content: 'Summarize the market', + }, + ], + budget: 'low', + include: ['requirements_result'], + }).success, + ).toBe(true); + expect( + StudioByAI21LabsEndpointInputSchemas.createMaestroRun.safeParse({ + input: 'Summarize the market', + models: ['jamba-mini'], + }).success, + ).toBe(true); + expect( + StudioByAI21LabsEndpointInputSchemas.createMaestroRun.safeParse({ + input: 'Summarize the market', + models: 'jamba-mini', + }).success, + ).toBe(false); + expect( + StudioByAI21LabsEndpointOutputSchemas.createMaestroRun.safeParse({ + id: 'run-1', + status: 'completed', + result: 'ok', + }).success, + ).toBe(true); + expect( + StudioByAI21LabsEndpointInputSchemas.retrieveMaestroRun.safeParse({ + id: 'run-1', + }).success, + ).toBe(true); + }); +}); + +describe('StudioByAI21Labs client', () => { + beforeEach(() => { + mockRequest.mockReset(); + mockRequest.mockResolvedValue({ ok: true }); + }); + + it('sends bearer auth and forwards query on every method', async () => { + await makeStudioByAI21LabsRequest('library/files', 'test-key', { + method: 'GET', + query: { limit: 5 }, + }); + + expect(mockRequest).toHaveBeenCalledWith( + expect.objectContaining({ + BASE: STUDIOBYAI21LABS_API_BASE, + TOKEN: 'test-key', + HEADERS: expect.objectContaining({ + Authorization: 'Bearer test-key', + }), + }), + expect.objectContaining({ + method: 'GET', + url: 'library/files', + query: { limit: 5 }, + }), + ); + }); + + it('rethrows ApiError so status is preserved', async () => { + const apiError = new ApiError( + { + method: 'GET', + url: 'library/files', + } as never, + { + url: 'https://api.ai21.com/studio/v1/library/files', + ok: false, + status: 429, + statusText: 'Too Many Requests', + body: { detail: 'rate limited' }, + } as never, + 'rate limited', + ); + mockRequest.mockRejectedValue(apiError); + + await expect( + makeStudioByAI21LabsRequest('library/files', 'test-key'), + ).rejects.toBe(apiError); + }); +}); + +describe('StudioByAI21Labs error handlers', () => { + it('matches 429 ApiError without treating arbitrary 429 text as a retry', async () => { + const rateLimited = new ApiError( + { + method: 'GET', + url: 'chat/completions', + } as never, + { + url: 'https://api.ai21.com/studio/v1/chat/completions', + ok: false, + status: 429, + statusText: 'Too Many Requests', + body: {}, + } as never, + 'Too Many Requests', + ); + + expect(errorHandlers.RATE_LIMIT_ERROR.match(rateLimited)).toBe(true); + expect( + errorHandlers.RATE_LIMIT_ERROR.match(new Error('id 4290 failed')), + ).toBe(false); + await expect( + errorHandlers.RATE_LIMIT_ERROR.handler(rateLimited), + ).resolves.toEqual({ maxRetries: 0, headersRetryAfterMs: undefined }); + }); +}); + +describe('StudioByAI21Labs keyBuilder', () => { + it('throws AuthMissingError when no key is configured', async () => { + const plugin = studiobyai21labs(); + const keyBuilder = plugin.keyBuilder; + if (!keyBuilder) throw new Error('keyBuilder missing'); + await expect( + keyBuilder( + { + authType: 'api_key', + keys: { + get_api_key: async () => undefined, + }, + } as never, + 'endpoint', + ), + ).rejects.toBeInstanceOf(AuthMissingError); + }); + + it('returns an explicit plugin key', async () => { + const plugin = studiobyai21labs({ key: 'explicit-key' }); + const keyBuilder = plugin.keyBuilder; + if (!keyBuilder) throw new Error('keyBuilder missing'); + await expect( + keyBuilder({ authType: 'api_key' } as never, 'endpoint'), + ).resolves.toBe('explicit-key'); + }); +}); + +describe('StudioByAI21Labs endpoint handlers', () => { + beforeEach(() => { + mockRequest.mockReset(); + mockLogEvent.mockClear(); + }); + + it('chat completions posts to the official path and redacts messages', async () => { + mockRequest.mockResolvedValue({ + id: 'cmpl-1', + choices: [{ message: { role: 'assistant', content: 'Hi' } }], + }); + + const messages = [{ role: 'user' as const, content: 'secret prompt' }]; + await Chat.completions(testCtx(), { + model: 'jamba-large', + messages, + max_tokens: 64, + }); + + expect(mockRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: 'POST', + url: 'chat/completions', + body: expect.objectContaining({ + model: 'jamba-large', + messages, + max_tokens: 64, + stream: false, + }), + }), + ); + expect(mockLogEvent).toHaveBeenCalledWith( + expect.anything(), + 'studiobyai21labs.chat.completions', + { model: 'jamba-large', n: undefined }, + 'completed', + ); + }); + + it('library list uses query filters', async () => { + mockRequest.mockResolvedValue([]); + await Library.list(testCtx(), { + status: 'PROCESSED', + label: ['invoices', 'Q3'], + limit: 10, + }); + expect(mockRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: 'GET', + url: 'library/files', + query: expect.objectContaining({ + status: 'PROCESSED', + label: 'invoices,Q3', + limit: 10, + }), + }), + ); + }); + + it('library upload posts JSON when only a public URL is provided', async () => { + mockRequest.mockResolvedValue({ id: 'file-1' }); + await Library.upload(testCtx(), { + publicUrl: 'https://example.com/file.pdf', + path: 'docs/file.pdf', + labels: ['docs'], + }); + expect(mockRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: 'POST', + url: 'library/files', + body: { + path: 'docs/file.pdf', + labels: ['docs'], + publicUrl: 'https://example.com/file.pdf', + }, + }), + ); + }); + + it('library upload sends multipart when a file is provided', async () => { + const fetchMock = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ id: 'file-2' }), + }); + const originalFetch = globalThis.fetch; + globalThis.fetch = fetchMock as typeof fetch; + + try { + await Library.upload(testCtx(), { + file: 'hello', + fileName: 'notes.txt', + path: 'docs/notes.txt', + labels: ['notes'], + }); + } finally { + globalThis.fetch = originalFetch; + } + + expect(mockRequest).not.toHaveBeenCalled(); + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.ai21.com/studio/v1/library/files', + expect.objectContaining({ + method: 'POST', + headers: { Authorization: 'Bearer test-key' }, + }), + ); + const body = fetchMock.mock.calls[0]?.[1]?.body as FormData; + expect(body.get('path')).toBe('docs/notes.txt'); + expect(body.get('labels')).toBe(JSON.stringify(['notes'])); + }); + + it('library get, update, delete, and download use file id paths', async () => { + mockRequest.mockResolvedValue({ id: 'file-1', name: 'notes.txt' }); + await Library.get(testCtx(), { file_id: 'file-1' }); + expect(mockRequest.mock.calls.at(-1)?.[1]).toMatchObject({ + method: 'GET', + url: 'library/files/file-1', + }); + + mockRequest.mockResolvedValue(undefined); + await Library.update(testCtx(), { + file_id: 'file-1', + labels: ['updated'], + }); + expect(mockRequest.mock.calls.at(-1)?.[1]).toMatchObject({ + method: 'PUT', + url: 'library/files/file-1', + body: { labels: ['updated'] }, + }); + + await Library.deleteFile(testCtx(), { file_id: 'file-1' }); + expect(mockRequest.mock.calls.at(-1)?.[1]).toMatchObject({ + method: 'DELETE', + url: 'library/files/file-1', + }); + + mockRequest.mockResolvedValue( + 'https://storage.ai21.com/files/file-1/download?token=xyz', + ); + const link = await Library.download(testCtx(), { file_id: 'file-1' }); + expect(mockRequest.mock.calls.at(-1)?.[1]).toMatchObject({ + method: 'GET', + url: 'library/files/file-1/download', + }); + expect(link).toBe( + 'https://storage.ai21.com/files/file-1/download?token=xyz', + ); + }); + + it('maestro create and retrieve use official run paths', async () => { + mockRequest.mockResolvedValue({ + id: 'run-1', + status: 'in_progress', + }); + await Maestro.createRun(testCtx(), { + input: [{ role: 'user', content: 'Summarize this' }], + budget: 'medium', + }); + expect(mockRequest.mock.calls.at(-1)?.[1]).toMatchObject({ + method: 'POST', + url: 'maestro/runs', + body: expect.objectContaining({ + input: [{ role: 'user', content: 'Summarize this' }], + budget: 'medium', + }), + }); + + mockRequest.mockResolvedValue({ + id: 'run-1', + status: 'completed', + result: 'done', + }); + await Maestro.retrieveRun(testCtx(), { id: 'run-1' }); + expect(mockRequest.mock.calls.at(-1)?.[1]).toMatchObject({ + method: 'GET', + url: 'maestro/runs/run-1', + }); + }); +}); diff --git a/packages/studiobyai21labs/client.ts b/packages/studiobyai21labs/client.ts new file mode 100644 index 000000000..c492e7e84 --- /dev/null +++ b/packages/studiobyai21labs/client.ts @@ -0,0 +1,103 @@ +import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http'; +import { ApiError, request } from 'corsair/http'; + +export class StudioByAI21LabsAPIError extends Error { + constructor( + message: string, + public readonly code?: string, + ) { + super(message); + this.name = 'StudioByAI21LabsAPIError'; + } +} + +export const STUDIOBYAI21LABS_API_BASE = 'https://api.ai21.com/studio/v1'; + +export async function makeStudioByAI21LabsRequest( + endpoint: string, + apiKey: string, + options: { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + body?: Record; + query?: Record; + } = {}, +): Promise { + const { method = 'GET', body, query } = options; + + const config: OpenAPIConfig = { + BASE: STUDIOBYAI21LABS_API_BASE, + VERSION: 'v1', + WITH_CREDENTIALS: false, + CREDENTIALS: 'omit', + TOKEN: apiKey, + HEADERS: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + }; + + const requestOptions: ApiRequestOptions = { + method, + url: endpoint, + body: + method === 'POST' || method === 'PUT' || method === 'PATCH' + ? body + : undefined, + mediaType: 'application/json; charset=utf-8', + query, + }; + + try { + return await request(config, requestOptions); + } catch (error) { + if (error instanceof ApiError) { + throw error; + } + if (error instanceof Error) { + throw new StudioByAI21LabsAPIError(error.message); + } + throw new StudioByAI21LabsAPIError('Unknown error'); + } +} + +const buildUrl = (endpoint: string): string => { + const baseUrl = STUDIOBYAI21LABS_API_BASE.endsWith('/') + ? STUDIOBYAI21LABS_API_BASE.slice(0, -1) + : STUDIOBYAI21LABS_API_BASE; + const path = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint; + return `${baseUrl}/${path}`; +}; + +export async function uploadStudioByAI21LabsFile( + endpoint: string, + apiKey: string, + options: { + file: Blob | string; + fileName: string; + fields?: Record; + }, +): Promise { + const { file, fileName, fields = {} } = options; + const blob = typeof file === 'string' ? new Blob([file]) : file; + + const formData = new FormData(); + formData.append('file', blob, fileName); + for (const [key, value] of Object.entries(fields)) { + if (value !== undefined) formData.append(key, value); + } + + const response = await fetch(buildUrl(endpoint), { + method: 'POST', + headers: { Authorization: `Bearer ${apiKey}` }, + body: formData, + }); + + if (!response.ok) { + const text = await response.text(); + throw new StudioByAI21LabsAPIError( + `Upload failed: status ${response.status}; body: ${text}`, + ); + } + + return response.json() as Promise; +} diff --git a/packages/studiobyai21labs/endpoints/chat.ts b/packages/studiobyai21labs/endpoints/chat.ts new file mode 100644 index 000000000..0f3a71bdb --- /dev/null +++ b/packages/studiobyai21labs/endpoints/chat.ts @@ -0,0 +1,36 @@ +import { logEventFromContext } from 'corsair/core'; +import type { StudioByAI21LabsEndpoints } from '..'; +import { makeStudioByAI21LabsRequest } from '../client'; +import type { StudioByAI21LabsEndpointOutputs } from './types'; + +export const completions: StudioByAI21LabsEndpoints['chatCompletions'] = async ( + ctx, + input, +) => { + const response = await makeStudioByAI21LabsRequest< + StudioByAI21LabsEndpointOutputs['chatCompletions'] + >('chat/completions', ctx.key, { + method: 'POST', + body: { + model: input.model, + messages: input.messages, + tools: input.tools, + documents: input.documents, + response_format: input.response_format, + max_tokens: input.max_tokens, + temperature: input.temperature, + top_p: input.top_p, + stop: input.stop, + n: input.n, + stream: false, + }, + }); + + await logEventFromContext( + ctx, + 'studiobyai21labs.chat.completions', + { model: input.model, n: input.n }, + 'completed', + ); + return response; +}; diff --git a/packages/studiobyai21labs/endpoints/index.ts b/packages/studiobyai21labs/endpoints/index.ts new file mode 100644 index 000000000..6273e47ba --- /dev/null +++ b/packages/studiobyai21labs/endpoints/index.ts @@ -0,0 +1,11 @@ +import { completions as chatCompletions } from './chat'; +import * as Library from './library'; +import * as Maestro from './maestro'; + +export const Chat = { + completions: chatCompletions, +}; + +export { Library, Maestro }; + +export * from './types'; diff --git a/packages/studiobyai21labs/endpoints/library.ts b/packages/studiobyai21labs/endpoints/library.ts new file mode 100644 index 000000000..1ed6c12b4 --- /dev/null +++ b/packages/studiobyai21labs/endpoints/library.ts @@ -0,0 +1,155 @@ +import { logEventFromContext } from 'corsair/core'; +import type { StudioByAI21LabsEndpoints } from '..'; +import { + makeStudioByAI21LabsRequest, + uploadStudioByAI21LabsFile, +} from '../client'; +import type { StudioByAI21LabsEndpointOutputs } from './types'; + +export const list: StudioByAI21LabsEndpoints['listLibraryFiles'] = async ( + ctx, + input, +) => { + const label = Array.isArray(input.label) + ? input.label.join(',') + : input.label; + const response = await makeStudioByAI21LabsRequest< + StudioByAI21LabsEndpointOutputs['listLibraryFiles'] + >('library/files', ctx.key, { + method: 'GET', + query: { + name: input.name, + path: input.path, + status: input.status, + label, + offset: input.offset, + limit: input.limit, + }, + }); + + await logEventFromContext( + ctx, + 'studiobyai21labs.library.list', + { name: input.name, path: input.path, status: input.status }, + 'completed', + ); + return response; +}; + +export const upload: StudioByAI21LabsEndpoints['uploadWorkspaceFile'] = async ( + ctx, + input, +) => { + const { file, fileName, path, labels, publicUrl } = input; + + if (file !== undefined) { + const fields: Record = { + path, + publicUrl, + labels: labels ? JSON.stringify(labels) : undefined, + }; + const response = await uploadStudioByAI21LabsFile< + StudioByAI21LabsEndpointOutputs['uploadWorkspaceFile'] + >('library/files', ctx.key, { + file, + fileName: fileName ?? 'upload', + fields, + }); + await logEventFromContext( + ctx, + 'studiobyai21labs.library.upload', + { fileName: fileName ?? 'upload', path, publicUrl }, + 'completed', + ); + return response; + } + + const response = await makeStudioByAI21LabsRequest< + StudioByAI21LabsEndpointOutputs['uploadWorkspaceFile'] + >('library/files', ctx.key, { + method: 'POST', + body: { + path, + labels, + publicUrl, + }, + }); + await logEventFromContext( + ctx, + 'studiobyai21labs.library.upload', + { path, publicUrl }, + 'completed', + ); + return response; +}; + +export const get: StudioByAI21LabsEndpoints['getWorkspaceFile'] = async ( + ctx, + input, +) => { + const response = await makeStudioByAI21LabsRequest< + StudioByAI21LabsEndpointOutputs['getWorkspaceFile'] + >(`library/files/${input.file_id}`, ctx.key, { method: 'GET' }); + + await logEventFromContext( + ctx, + 'studiobyai21labs.library.get', + { file_id: input.file_id }, + 'completed', + ); + return response; +}; + +export const update: StudioByAI21LabsEndpoints['updateFile'] = async ( + ctx, + input, +) => { + const { file_id, publicUrl, labels } = input; + await makeStudioByAI21LabsRequest( + `library/files/${file_id}`, + ctx.key, + { method: 'PUT', body: { publicUrl, labels } }, + ); + + await logEventFromContext( + ctx, + 'studiobyai21labs.library.update', + { file_id }, + 'completed', + ); + return undefined; +}; + +export const deleteFile: StudioByAI21LabsEndpoints['deleteFile'] = async ( + ctx, + input, +) => { + await makeStudioByAI21LabsRequest( + `library/files/${input.file_id}`, + ctx.key, + { method: 'DELETE' }, + ); + + await logEventFromContext( + ctx, + 'studiobyai21labs.library.delete', + { file_id: input.file_id }, + 'completed', + ); + return undefined; +}; + +export const download: StudioByAI21LabsEndpoints['getFileDownloadLink'] = + async (ctx, input) => { + const response = await makeStudioByAI21LabsRequest< + StudioByAI21LabsEndpointOutputs['getFileDownloadLink'] + >(`library/files/${input.file_id}/download`, ctx.key, { method: 'GET' }); + + await logEventFromContext( + ctx, + 'studiobyai21labs.library.download', + { file_id: input.file_id }, + 'completed', + ); + return response; + }; diff --git a/packages/studiobyai21labs/endpoints/maestro.ts b/packages/studiobyai21labs/endpoints/maestro.ts new file mode 100644 index 000000000..1a4cb7c22 --- /dev/null +++ b/packages/studiobyai21labs/endpoints/maestro.ts @@ -0,0 +1,52 @@ +import { logEventFromContext } from 'corsair/core'; +import type { StudioByAI21LabsEndpoints } from '..'; +import { makeStudioByAI21LabsRequest } from '../client'; +import type { StudioByAI21LabsEndpointOutputs } from './types'; + +export const createRun: StudioByAI21LabsEndpoints['createMaestroRun'] = async ( + ctx, + input, +) => { + const response = await makeStudioByAI21LabsRequest< + StudioByAI21LabsEndpointOutputs['createMaestroRun'] + >('maestro/runs', ctx.key, { + method: 'POST', + body: { + input: input.input, + system_prompt: input.system_prompt, + requirements: input.requirements, + tools: input.tools, + models: input.models, + budget: input.budget, + include: input.include, + response_language: input.response_language, + }, + }); + + await logEventFromContext( + ctx, + 'studiobyai21labs.maestro.createRun', + { + budget: input.budget, + models: input.models, + hasRequirements: Boolean(input.requirements?.length), + }, + 'completed', + ); + return response; +}; + +export const retrieveRun: StudioByAI21LabsEndpoints['retrieveMaestroRun'] = + async (ctx, input) => { + const response = await makeStudioByAI21LabsRequest< + StudioByAI21LabsEndpointOutputs['retrieveMaestroRun'] + >(`maestro/runs/${input.id}`, ctx.key, { method: 'GET' }); + + await logEventFromContext( + ctx, + 'studiobyai21labs.maestro.retrieveRun', + { id: input.id }, + 'completed', + ); + return response; + }; diff --git a/packages/studiobyai21labs/endpoints/types.ts b/packages/studiobyai21labs/endpoints/types.ts new file mode 100644 index 000000000..e3a3c0c98 --- /dev/null +++ b/packages/studiobyai21labs/endpoints/types.ts @@ -0,0 +1,382 @@ +import { z } from 'zod'; + +const ChatMessageSchema = z + .object({ + role: z.enum(['system', 'user', 'assistant', 'tool']), + content: z.string().nullable().optional(), + tool_calls: z + .array( + z + .object({ + id: z.string(), + type: z.string(), + function: z + .object({ + name: z.string(), + arguments: z.string(), + }) + .loose(), + }) + .loose(), + ) + .optional(), + tool_call_id: z.string().optional(), + }) + .loose(); + +const ChatToolSchema = z + .object({ + type: z.literal('function'), + function: z + .object({ + name: z.string(), + description: z.string().optional(), + parameters: z.record(z.string(), z.unknown()).optional(), + }) + .loose(), + }) + .loose(); + +const ChatDocumentSchema = z + .object({ + content: z.string(), + metadata: z.record(z.string(), z.unknown()).optional(), + }) + .loose(); + +const ChatCompletionsInputSchema = z + .object({ + model: z.string(), + messages: z.array(ChatMessageSchema).min(1), + tools: z.array(ChatToolSchema).optional(), + documents: z.array(ChatDocumentSchema).optional(), + response_format: z + .object({ + type: z.enum(['text', 'json_object']), + }) + .optional(), + max_tokens: z.number().int().min(1).max(4096).optional(), + temperature: z.number().min(0).max(2).optional(), + top_p: z.number().min(0).max(1).optional(), + stop: z.union([z.string(), z.array(z.string())]).optional(), + n: z.number().int().min(1).max(16).optional(), + }) + .strict(); +export type ChatCompletionsInput = z.infer; + +const ChatCompletionsResponseSchema = z + .object({ + id: z.string().optional(), + choices: z.array( + z + .object({ + index: z.number().optional(), + message: z + .object({ + role: z.string().optional(), + content: z.string().nullable().optional(), + tool_calls: z.array(z.unknown()).optional(), + }) + .loose() + .optional(), + finish_reason: z.string().optional(), + }) + .loose(), + ), + usage: z + .object({ + prompt_tokens: z.number().optional(), + completion_tokens: z.number().optional(), + total_tokens: z.number().optional(), + }) + .loose() + .optional(), + }) + .loose(); +export type ChatCompletionsResponse = z.infer< + typeof ChatCompletionsResponseSchema +>; + +const FileMetadataSchema = z + .object({ + id: z.string(), + name: z.string().optional(), + size: z.number().optional(), + created_at: z.string().optional(), + labels: z.array(z.string()).optional(), + errorCode: z.string().optional(), + errorMessage: z.string().optional(), + }) + .loose(); +export type FileMetadata = z.infer; + +const ListLibraryFilesInputSchema = z.object({ + name: z.string().optional(), + path: z.string().optional(), + status: z + .enum([ + 'DB_RECORD_CREATED', + 'UPLOADED', + 'UPLOAD_FAILED', + 'PROCESSED', + 'PROCESSING_FAILED', + ]) + .optional(), + label: z.union([z.string(), z.array(z.string())]).optional(), + offset: z.number().int().min(0).optional(), + limit: z.number().int().min(1).max(1000).optional(), +}); +export type ListLibraryFilesInput = z.infer; + +const ListLibraryFilesResponseSchema = z.array(FileMetadataSchema); +export type ListLibraryFilesResponse = z.infer< + typeof ListLibraryFilesResponseSchema +>; + +const UploadWorkspaceFileInputSchema = z + .object({ + file: z + .union([z.string(), z.custom((value) => value instanceof Blob)]) + .optional(), + fileName: z.string().optional(), + path: z.string().optional(), + labels: z.array(z.string()).optional(), + publicUrl: z.string().optional(), + }) + .refine( + (value) => value.file !== undefined || value.publicUrl !== undefined, + { + message: 'Either file or publicUrl is required', + }, + ); +export type UploadWorkspaceFileInput = z.infer< + typeof UploadWorkspaceFileInputSchema +>; + +const UploadWorkspaceFileResponseSchema = z + .object({ + id: z.string(), + }) + .loose(); +export type UploadWorkspaceFileResponse = z.infer< + typeof UploadWorkspaceFileResponseSchema +>; + +const GetWorkspaceFileInputSchema = z.object({ + file_id: z.string(), +}); +export type GetWorkspaceFileInput = z.infer; + +const GetWorkspaceFileResponseSchema = FileMetadataSchema; +export type GetWorkspaceFileResponse = z.infer< + typeof GetWorkspaceFileResponseSchema +>; + +const UpdateFileInputSchema = z.object({ + file_id: z.string(), + publicUrl: z.string().optional(), + labels: z.array(z.string()).optional(), +}); +export type UpdateFileInput = z.infer; + +const UpdateFileResponseSchema = z.undefined(); +export type UpdateFileResponse = z.infer; + +const DeleteFileInputSchema = z.object({ + file_id: z.string(), +}); +export type DeleteFileInput = z.infer; + +const DeleteFileResponseSchema = z.undefined(); +export type DeleteFileResponse = z.infer; + +const GetFileDownloadLinkInputSchema = z.object({ + file_id: z.string(), +}); +export type GetFileDownloadLinkInput = z.infer< + typeof GetFileDownloadLinkInputSchema +>; + +const GetFileDownloadLinkResponseSchema = z.string(); +export type GetFileDownloadLinkResponse = z.infer< + typeof GetFileDownloadLinkResponseSchema +>; + +const MaestroMessageSchema = z + .object({ + role: z.enum(['user', 'assistant']), + content: z.string(), + }) + .loose(); + +const MaestroRequirementSchema = z + .object({ + name: z.string(), + description: z.string(), + is_mandatory: z.boolean().optional(), + }) + .loose(); + +const MaestroToolSchema = z.union([ + z + .object({ + type: z.literal('mcp'), + server_label: z.string(), + server_url: z.string(), + headers: z.record(z.string(), z.string()).optional(), + allowed_tools: z.array(z.string()).optional(), + }) + .loose(), + z + .object({ + type: z.literal('http'), + function: z + .object({ + name: z.string(), + description: z.string().optional(), + parameters: z.record(z.string(), z.unknown()).optional(), + }) + .loose(), + endpoint: z + .object({ + url: z.string(), + headers: z.record(z.string(), z.string()).optional(), + }) + .loose(), + }) + .loose(), + z + .object({ + type: z.literal('file_search'), + labels: z.array(z.string()).optional(), + file_ids: z.array(z.string()).optional(), + }) + .loose(), + z + .object({ + type: z.literal('web_search'), + urls: z.array(z.string()).optional(), + }) + .loose(), +]); + +const CreateMaestroRunInputSchema = z.object({ + input: z.union([z.string(), z.array(MaestroMessageSchema)]), + system_prompt: z.string().optional(), + requirements: z.array(MaestroRequirementSchema).max(10).optional(), + tools: z.array(MaestroToolSchema).optional(), + models: z.array(z.string()).optional(), + budget: z.enum(['low', 'medium', 'high']).optional(), + include: z.array(z.enum(['data_sources', 'requirements_result'])).optional(), + response_language: z + .enum([ + 'arabic', + 'dutch', + 'english', + 'french', + 'german', + 'hebrew', + 'italian', + 'portuguese', + 'spanish', + ]) + .optional(), +}); +export type CreateMaestroRunInput = z.infer; + +const MaestroRunSchema = z + .object({ + id: z.string(), + status: z.enum(['completed', 'failed', 'in_progress']).optional(), + result: z.unknown().optional(), + requirements_result: z + .object({ + score: z.number().optional(), + finish_reason: z.string().optional(), + requirements: z.array(z.unknown()).optional(), + }) + .loose() + .optional(), + data_sources: z + .object({ + web_search: z.array(z.unknown()).optional(), + file_search: z.array(z.unknown()).optional(), + tool_calls: z.array(z.unknown()).optional(), + }) + .loose() + .optional(), + error: z + .object({ + message: z.string().optional(), + }) + .loose() + .nullable() + .optional(), + }) + .loose(); +export type MaestroRun = z.infer; + +const CreateMaestroRunResponseSchema = MaestroRunSchema; +export type CreateMaestroRunResponse = z.infer< + typeof CreateMaestroRunResponseSchema +>; + +const RetrieveMaestroRunInputSchema = z.object({ + id: z.string(), +}); +export type RetrieveMaestroRunInput = z.infer< + typeof RetrieveMaestroRunInputSchema +>; + +const RetrieveMaestroRunResponseSchema = MaestroRunSchema; +export type RetrieveMaestroRunResponse = z.infer< + typeof RetrieveMaestroRunResponseSchema +>; + +export type StudioByAI21LabsEndpointInputs = { + chatCompletions: ChatCompletionsInput; + listLibraryFiles: ListLibraryFilesInput; + uploadWorkspaceFile: UploadWorkspaceFileInput; + getWorkspaceFile: GetWorkspaceFileInput; + updateFile: UpdateFileInput; + deleteFile: DeleteFileInput; + getFileDownloadLink: GetFileDownloadLinkInput; + createMaestroRun: CreateMaestroRunInput; + retrieveMaestroRun: RetrieveMaestroRunInput; +}; + +export type StudioByAI21LabsEndpointOutputs = { + chatCompletions: ChatCompletionsResponse; + listLibraryFiles: ListLibraryFilesResponse; + uploadWorkspaceFile: UploadWorkspaceFileResponse; + getWorkspaceFile: GetWorkspaceFileResponse; + updateFile: UpdateFileResponse; + deleteFile: DeleteFileResponse; + getFileDownloadLink: GetFileDownloadLinkResponse; + createMaestroRun: CreateMaestroRunResponse; + retrieveMaestroRun: RetrieveMaestroRunResponse; +}; + +export const StudioByAI21LabsEndpointInputSchemas = { + chatCompletions: ChatCompletionsInputSchema, + listLibraryFiles: ListLibraryFilesInputSchema, + uploadWorkspaceFile: UploadWorkspaceFileInputSchema, + getWorkspaceFile: GetWorkspaceFileInputSchema, + updateFile: UpdateFileInputSchema, + deleteFile: DeleteFileInputSchema, + getFileDownloadLink: GetFileDownloadLinkInputSchema, + createMaestroRun: CreateMaestroRunInputSchema, + retrieveMaestroRun: RetrieveMaestroRunInputSchema, +} as const; + +export const StudioByAI21LabsEndpointOutputSchemas = { + chatCompletions: ChatCompletionsResponseSchema, + listLibraryFiles: ListLibraryFilesResponseSchema, + uploadWorkspaceFile: UploadWorkspaceFileResponseSchema, + getWorkspaceFile: GetWorkspaceFileResponseSchema, + updateFile: UpdateFileResponseSchema, + deleteFile: DeleteFileResponseSchema, + getFileDownloadLink: GetFileDownloadLinkResponseSchema, + createMaestroRun: CreateMaestroRunResponseSchema, + retrieveMaestroRun: RetrieveMaestroRunResponseSchema, +} as const; diff --git a/packages/studiobyai21labs/error-handlers.ts b/packages/studiobyai21labs/error-handlers.ts new file mode 100644 index 000000000..2f187afa0 --- /dev/null +++ b/packages/studiobyai21labs/error-handlers.ts @@ -0,0 +1,31 @@ +import type { CorsairErrorHandler } from 'corsair/core'; +import { ApiError } from 'corsair/http'; + +export const errorHandlers = { + RATE_LIMIT_ERROR: { + match: (error: Error) => { + if (error instanceof ApiError && error.status === 429) return true; + const msg = error.message.toLowerCase(); + return msg.includes('rate_limit') || msg.includes('rate limit'); + }, + handler: async (error: Error) => { + let retryAfterMs: number | undefined; + if (error instanceof ApiError && error.retryAfter !== undefined) { + retryAfterMs = error.retryAfter; + } + return { maxRetries: 0, headersRetryAfterMs: retryAfterMs }; + }, + }, + AUTH_ERROR: { + match: (error: Error) => { + if (error instanceof ApiError && error.status === 401) return true; + const msg = error.message.toLowerCase(); + return msg.includes('unauthorized') || msg.includes('invalid_auth'); + }, + handler: async () => ({ maxRetries: 0 }), + }, + DEFAULT: { + match: () => true, + handler: async () => ({ maxRetries: 0 }), + }, +} satisfies CorsairErrorHandler; diff --git a/packages/studiobyai21labs/index.ts b/packages/studiobyai21labs/index.ts new file mode 100644 index 000000000..6b6738268 --- /dev/null +++ b/packages/studiobyai21labs/index.ts @@ -0,0 +1,266 @@ +import type { + AuthTypes, + BindEndpoints, + CorsairEndpoint, + CorsairErrorHandler, + CorsairPlugin, + CorsairPluginContext, + KeyBuilderContext, + PickAuth, + PluginAuthConfig, + PluginPermissionsConfig, + RequiredPluginEndpointMeta, + RequiredPluginEndpointSchemas, +} from 'corsair/core'; +import { AuthMissingError } from 'corsair/core'; +import { Chat, Library, Maestro } from './endpoints'; +import type { + StudioByAI21LabsEndpointInputs, + StudioByAI21LabsEndpointOutputs, +} from './endpoints/types'; +import { + StudioByAI21LabsEndpointInputSchemas, + StudioByAI21LabsEndpointOutputSchemas, +} from './endpoints/types'; +import { errorHandlers } from './error-handlers'; +import { StudioByAI21LabsSchema } from './schema'; + +export type StudioByAI21LabsPluginOptions = { + authType?: PickAuth<'api_key'>; + key?: string; + hooks?: InternalStudioByAI21LabsPlugin['hooks']; + errorHandlers?: CorsairErrorHandler; + permissions?: PluginPermissionsConfig; +}; + +export type StudioByAI21LabsContext = CorsairPluginContext< + typeof StudioByAI21LabsSchema, + StudioByAI21LabsPluginOptions +>; + +export type StudioByAI21LabsKeyBuilderContext = + KeyBuilderContext; + +export type StudioByAI21LabsBoundEndpoints = BindEndpoints< + typeof studioByAI21LabsEndpointsNested +>; + +type StudioByAI21LabsEndpoint = + CorsairEndpoint< + StudioByAI21LabsContext, + StudioByAI21LabsEndpointInputs[K], + StudioByAI21LabsEndpointOutputs[K] + >; + +export type StudioByAI21LabsEndpoints = { + chatCompletions: StudioByAI21LabsEndpoint<'chatCompletions'>; + listLibraryFiles: StudioByAI21LabsEndpoint<'listLibraryFiles'>; + uploadWorkspaceFile: StudioByAI21LabsEndpoint<'uploadWorkspaceFile'>; + getWorkspaceFile: StudioByAI21LabsEndpoint<'getWorkspaceFile'>; + updateFile: StudioByAI21LabsEndpoint<'updateFile'>; + deleteFile: StudioByAI21LabsEndpoint<'deleteFile'>; + getFileDownloadLink: StudioByAI21LabsEndpoint<'getFileDownloadLink'>; + createMaestroRun: StudioByAI21LabsEndpoint<'createMaestroRun'>; + retrieveMaestroRun: StudioByAI21LabsEndpoint<'retrieveMaestroRun'>; +}; + +const studioByAI21LabsEndpointsNested = { + chat: { + completions: Chat.completions, + }, + library: { + list: Library.list, + upload: Library.upload, + get: Library.get, + update: Library.update, + delete: Library.deleteFile, + download: Library.download, + }, + maestro: { + createRun: Maestro.createRun, + retrieveRun: Maestro.retrieveRun, + }, +} as const; + +export const studioByAI21LabsEndpointSchemas = { + 'chat.completions': { + input: StudioByAI21LabsEndpointInputSchemas.chatCompletions, + output: StudioByAI21LabsEndpointOutputSchemas.chatCompletions, + }, + 'library.list': { + input: StudioByAI21LabsEndpointInputSchemas.listLibraryFiles, + output: StudioByAI21LabsEndpointOutputSchemas.listLibraryFiles, + }, + 'library.upload': { + input: StudioByAI21LabsEndpointInputSchemas.uploadWorkspaceFile, + output: StudioByAI21LabsEndpointOutputSchemas.uploadWorkspaceFile, + }, + 'library.get': { + input: StudioByAI21LabsEndpointInputSchemas.getWorkspaceFile, + output: StudioByAI21LabsEndpointOutputSchemas.getWorkspaceFile, + }, + 'library.update': { + input: StudioByAI21LabsEndpointInputSchemas.updateFile, + output: StudioByAI21LabsEndpointOutputSchemas.updateFile, + }, + 'library.delete': { + input: StudioByAI21LabsEndpointInputSchemas.deleteFile, + output: StudioByAI21LabsEndpointOutputSchemas.deleteFile, + }, + 'library.download': { + input: StudioByAI21LabsEndpointInputSchemas.getFileDownloadLink, + output: StudioByAI21LabsEndpointOutputSchemas.getFileDownloadLink, + }, + 'maestro.createRun': { + input: StudioByAI21LabsEndpointInputSchemas.createMaestroRun, + output: StudioByAI21LabsEndpointOutputSchemas.createMaestroRun, + }, + 'maestro.retrieveRun': { + input: StudioByAI21LabsEndpointInputSchemas.retrieveMaestroRun, + output: StudioByAI21LabsEndpointOutputSchemas.retrieveMaestroRun, + }, +} as const satisfies RequiredPluginEndpointSchemas< + typeof studioByAI21LabsEndpointsNested +>; + +const defaultAuthType: AuthTypes = 'api_key' as const; + +const studioByAI21LabsEndpointMeta = { + 'chat.completions': { + riskLevel: 'write', + description: 'Generate a Jamba chat completion from a conversation history', + }, + 'library.list': { + riskLevel: 'read', + description: 'List workspace library files with optional filters', + }, + 'library.upload': { + riskLevel: 'write', + description: 'Upload a file or register a public URL in the library', + }, + 'library.get': { + riskLevel: 'read', + description: 'Get metadata for a library file', + }, + 'library.update': { + riskLevel: 'write', + description: 'Update a library file public URL or labels', + }, + 'library.delete': { + riskLevel: 'write', + description: 'Delete a library file', + }, + 'library.download': { + riskLevel: 'read', + description: 'Get a signed download URL for a library file', + }, + 'maestro.createRun': { + riskLevel: 'write', + description: 'Create an AI21 Maestro run', + }, + 'maestro.retrieveRun': { + riskLevel: 'read', + description: 'Retrieve an AI21 Maestro run by id', + }, +} as const satisfies RequiredPluginEndpointMeta< + typeof studioByAI21LabsEndpointsNested +>; + +export const studioByAI21LabsAuthConfig = { + api_key: { + account: ['one'] as const, + }, +} as const satisfies PluginAuthConfig; + +export type BaseStudioByAI21LabsPlugin< + T extends StudioByAI21LabsPluginOptions, +> = CorsairPlugin< + 'studiobyai21labs', + typeof StudioByAI21LabsSchema, + typeof studioByAI21LabsEndpointsNested, + {}, + T, + typeof defaultAuthType, + typeof studioByAI21LabsAuthConfig +>; + +export type InternalStudioByAI21LabsPlugin = + BaseStudioByAI21LabsPlugin; + +export type ExternalStudioByAI21LabsPlugin< + T extends StudioByAI21LabsPluginOptions, +> = BaseStudioByAI21LabsPlugin; + +export function studiobyai21labs( + incomingOptions: StudioByAI21LabsPluginOptions & + T = {} as StudioByAI21LabsPluginOptions & T, +): ExternalStudioByAI21LabsPlugin { + const options = { + ...incomingOptions, + authType: incomingOptions.authType ?? defaultAuthType, + }; + + return { + id: 'studiobyai21labs', + schema: StudioByAI21LabsSchema, + options, + hooks: options.hooks, + endpoints: studioByAI21LabsEndpointsNested, + webhooks: {}, + endpointMeta: studioByAI21LabsEndpointMeta, + endpointSchemas: studioByAI21LabsEndpointSchemas, + authConfig: studioByAI21LabsAuthConfig, + pluginWebhookMatcher: () => false, + errorHandlers: (() => { + const { DEFAULT: defaultHandler, ...specificDefaults } = errorHandlers; + return { + ...specificDefaults, + ...(options.errorHandlers || {}), + DEFAULT: options.errorHandlers?.DEFAULT || defaultHandler, + }; + })(), + keyBuilder: async (ctx: StudioByAI21LabsKeyBuilderContext, source) => { + if (source === 'endpoint' && options.key) { + return options.key; + } + + if (source === 'endpoint' && ctx.authType === 'api_key') { + const key = await ctx.keys.get_api_key(); + if (!key) { + throw new AuthMissingError('studiobyai21labs', 'api_key'); + } + return key; + } + + throw new AuthMissingError('studiobyai21labs', 'api_key'); + }, + } satisfies InternalStudioByAI21LabsPlugin; +} + +export type { + ChatCompletionsInput, + ChatCompletionsResponse, + CreateMaestroRunInput, + CreateMaestroRunResponse, + DeleteFileInput, + DeleteFileResponse, + GetFileDownloadLinkInput, + GetFileDownloadLinkResponse, + GetWorkspaceFileInput, + GetWorkspaceFileResponse, + ListLibraryFilesInput, + ListLibraryFilesResponse, + RetrieveMaestroRunInput, + RetrieveMaestroRunResponse, + StudioByAI21LabsEndpointInputs, + StudioByAI21LabsEndpointOutputs, + UpdateFileInput, + UpdateFileResponse, + UploadWorkspaceFileInput, + UploadWorkspaceFileResponse, +} from './endpoints/types'; + +export { + StudioByAI21LabsEndpointInputSchemas, + StudioByAI21LabsEndpointOutputSchemas, +} from './endpoints/types'; diff --git a/packages/studiobyai21labs/jest.config.cjs b/packages/studiobyai21labs/jest.config.cjs new file mode 100644 index 000000000..8c6218f64 --- /dev/null +++ b/packages/studiobyai21labs/jest.config.cjs @@ -0,0 +1,55 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: [''], + testMatch: [ + '**/*.test.ts', + '**/tests/**/*.test.ts', + '**/plugins/**/*.test.ts', + '**/setup/**/*.test.ts', + ], + collectCoverageFrom: [ + '**/*.ts', + '!**/*.d.ts', + '!**/node_modules/**', + '!**/dist/**', + '!jest.config.ts', + '!tests/**', + ], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], + transform: { + '^.+\\.yaml$': '/../corsair/jest-yaml-transform.cjs', + '^.+\\.ts$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + esModuleInterop: true, + allowSyntheticDefaultImports: true, + verbatimModuleSyntax: false, + module: 'ESNext', + moduleResolution: 'Bundler', + }, + }, + ], + '.*\\.js$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + esModuleInterop: true, + allowSyntheticDefaultImports: true, + }, + }, + ], + }, + moduleNameMapper: { + '^corsair/core$': '/../corsair/core.ts', + '^corsair/http$': '/../corsair/http.ts', + '^(\\.\\.?/.*)\\.js$': '$1', + }, + transformIgnorePatterns: ['node_modules/(?!.*uuid.*)'], + extensionsToTreatAsEsm: ['.ts'], + testTimeout: 30000, + verbose: true, +}; diff --git a/packages/studiobyai21labs/package.json b/packages/studiobyai21labs/package.json new file mode 100644 index 000000000..7affd18e1 --- /dev/null +++ b/packages/studiobyai21labs/package.json @@ -0,0 +1,44 @@ +{ + "name": "@corsair-dev/studiobyai21labs", + "version": "0.1.0", + "description": "StudioByAI21Labs plugin for Corsair", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "dev-source": "./index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "rm -rf dist && tsc --build --force && tsup", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "peerDependencies": { + "corsair": ">=0.1.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "corsair": "workspace:*", + "jest": "^29.7.0", + "ts-jest": "^29.4.9", + "tsup": "^8.0.1", + "typescript": "catalog:", + "zod": "^4.1.13" + }, + "keywords": [ + "corsair", + "studiobyai21labs", + "plugin" + ], + "author": "", + "license": "Apache-2.0", + "files": [ + "dist" + ] +} diff --git a/packages/studiobyai21labs/schema/database.ts b/packages/studiobyai21labs/schema/database.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/studiobyai21labs/schema/database.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/studiobyai21labs/schema/index.ts b/packages/studiobyai21labs/schema/index.ts new file mode 100644 index 000000000..d2bd3a75b --- /dev/null +++ b/packages/studiobyai21labs/schema/index.ts @@ -0,0 +1,4 @@ +export const StudioByAI21LabsSchema = { + version: '1.0.0', + entities: {}, +} as const; diff --git a/packages/studiobyai21labs/tsconfig.json b/packages/studiobyai21labs/tsconfig.json new file mode 100644 index 000000000..15e507a13 --- /dev/null +++ b/packages/studiobyai21labs/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["esnext"], + "types": ["node", "jest"], + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist", + "rootDir": "./", + "composite": true, + "incremental": true, + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true + }, + "include": ["./**/*"], + "exclude": ["dist", "node_modules"], + "references": [] +} diff --git a/packages/studiobyai21labs/tsup.config.ts b/packages/studiobyai21labs/tsup.config.ts new file mode 100644 index 000000000..3ec221e23 --- /dev/null +++ b/packages/studiobyai21labs/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + clean: false, + dts: false, + format: ['esm'], + target: 'esnext', + platform: 'node', + bundle: true, + splitting: true, + minify: true, + outDir: 'dist', + external: ['corsair', 'zod'], + entry: ['index.ts'], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0779d3702..9f8349cf2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4036,6 +4036,30 @@ importers: specifier: ^6.0.0 version: 6.4.2(@types/node@24.10.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.9.0) + packages/studiobyai21labs: + devDependencies: + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + corsair: + specifier: workspace:* + version: link:../corsair + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + ts-jest: + specifier: ^29.4.9 + version: 29.4.9(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.27.0)(jest-util@30.4.1)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3) + tsup: + specifier: ^8.0.1 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: 'catalog:' + version: 5.9.3 + zod: + specifier: 4.4.3 + version: 4.4.3 + packages/supabase: devDependencies: '@types/jest':