Ver código fonte

feat: add public endpoint for models list (#1228)

* feat: add public endpoint for models list

- Created ChatAPIService for public endpoints\n- Added /chat/models and /chat/models/details endpoints\n- Registered service in CoreModule\n- Added tests for the new service\n\nCloses #1227

ai: true

* Update src/backend/src/services/ChatAPIService.js
Eric Dubé 1 mês atrás
pai
commit
45c072ff93

+ 3 - 0
src/backend/src/CoreModule.js

@@ -367,6 +367,9 @@ const install = async ({ services, app, useapi, modapi }) => {
 
     const { ThreadService } = require('./services/ThreadService');
     services.registerService('thread', ThreadService);
+
+    const { ChatAPIService } = require('./services/ChatAPIService');
+    services.registerService('__chat-api', ChatAPIService);
 }
 
 const install_legacy = async ({ services }) => {

+ 115 - 0
src/backend/src/services/ChatAPIService.js

@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ * 
+ * This file is part of Puter.
+ * 
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+// METADATA // {"ai-commented":{"service":"claude"}}
+const { Endpoint } = require("../util/expressutil");
+const BaseService = require("./BaseService");
+const APIError = require("../api/APIError");
+
+/**
+* @class ChatAPIService
+* @extends BaseService
+* @description Service class that handles public (unauthenticated) API endpoints for AI chat functionality.
+* This service provides endpoints for retrieving available AI chat models without requiring authentication.
+*/
+class ChatAPIService extends BaseService {
+    static MODULES = {
+        express: require('express'),
+    };
+
+    /**
+    * Installs routes for chat API endpoints into the Express app
+    * @param {Object} _ Unused parameter
+    * @param {Object} options Installation options
+    * @param {Express} options.app Express application instance to install routes on
+    * @returns {Promise<void>}
+    */
+    async ['__on_install.routes'] (_, { app }) {
+        // Create a router for chat API endpoints
+        const router = (() => {
+            const require = this.require;
+            const express = require('express');
+            return express.Router();
+        })();
+
+        // Register the router with the Express app
+        app.use('/puterai/chat', router);
+
+        // Install endpoints
+        this.install_chat_endpoints_({ router });
+    }
+
+    /**
+    * Installs chat API endpoints on the provided router
+    * @param {Object} options Options object
+    * @param {express.Router} options.router Express router to install endpoints on
+    * @private
+    */
+    install_chat_endpoints_ ({ router }) {
+        // Endpoint to list available AI chat models
+        Endpoint({
+            route: '/models',
+            methods: ['GET'],
+            handler: async (req, res) => {
+                try {
+                    // Use SUService to access AIChatService as system user
+                    const svc_su = this.services.get('su');
+                    const models = await svc_su.sudo(async () => {
+                        const svc_aiChat = this.services.get('ai-chat');
+                        // Return the simple model list which contains basic model information
+                        return svc_aiChat.simple_model_list;
+                    });
+
+                    // Return the list of models
+                    res.json({ models });
+                } catch (error) {
+                    this.log.error('Error fetching models:', error);
+                    throw APIError.create('internal_server_error');
+                }
+            }
+        }).attach(router);
+
+        // Endpoint to get detailed information about available AI chat models
+        Endpoint({
+            route: '/models/details',
+            methods: ['GET'],
+            handler: async (req, res) => {
+                try {
+                    // Use SUService to access AIChatService as system user
+                    const svc_su = this.services.get('su');
+                    const models = await svc_su.sudo(async () => {
+                        const svc_aiChat = this.services.get('ai-chat');
+                        // Return the detailed model list which includes cost and capability information
+                        return svc_aiChat.detail_model_list;
+                    });
+
+                    // Return the detailed list of models
+                    res.json({ models });
+                } catch (error) {
+                    this.log.error('Error fetching model details:', error);
+                    throw APIError.create('internal_server_error');
+                }
+            }
+        }).attach(router);
+    }
+}
+
+module.exports = {
+    ChatAPIService,
+};

+ 166 - 0
src/backend/src/services/ChatAPIService.test.js

@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ * 
+ * This file is part of Puter.
+ * 
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+const { ChatAPIService } = require('./ChatAPIService');
+
+describe('ChatAPIService', () => {
+    let chatApiService;
+    let mockServices;
+    let mockRouter;
+    let mockApp;
+    let mockSUService;
+    let mockAIChatService;
+    let mockEndpoint;
+    let mockReq;
+    let mockRes;
+
+    beforeEach(() => {
+        // Mock AIChatService
+        mockAIChatService = {
+            simple_model_list: ['model1', 'model2'],
+            detail_model_list: [
+                { id: 'model1', name: 'Model 1', cost: { input: 1, output: 2 } },
+                { id: 'model2', name: 'Model 2', cost: { input: 3, output: 4 } }
+            ]
+        };
+
+        // Mock SUService
+        mockSUService = {
+            sudo: jest.fn().mockImplementation(async (callback) => {
+                if (typeof callback === 'function') {
+                    return await callback();
+                }
+                return await mockSUService.sudo.mockImplementation(async (cb) => await cb());
+            })
+        };
+
+        // Mock services
+        mockServices = {
+            get: jest.fn().mockImplementation((serviceName) => {
+                if (serviceName === 'su') return mockSUService;
+                if (serviceName === 'ai-chat') return mockAIChatService;
+                return null;
+            })
+        };
+
+        // Mock router and app
+        mockRouter = {
+            use: jest.fn(),
+            get: jest.fn(),
+            post: jest.fn()
+        };
+        mockApp = {
+            use: jest.fn()
+        };
+
+        // Mock Endpoint function
+        mockEndpoint = jest.fn().mockReturnValue({
+            attach: jest.fn()
+        });
+
+        // Mock request and response
+        mockReq = {};
+        mockRes = {
+            json: jest.fn()
+        };
+
+        // Setup ChatAPIService
+        chatApiService = new ChatAPIService();
+        chatApiService.services = mockServices;
+        chatApiService.log = {
+            error: jest.fn()
+        };
+        
+        // Mock the require function
+        chatApiService.require = jest.fn().mockImplementation((module) => {
+            if (module === 'express') return { Router: () => mockRouter };
+            return require(module);
+        });
+    });
+
+    describe('install_chat_endpoints_', () => {
+        it('should attach models endpoint to router', () => {
+            // Setup
+            global.Endpoint = mockEndpoint;
+
+            // Execute
+            chatApiService.install_chat_endpoints_({ router: mockRouter });
+
+            // Verify
+            expect(mockEndpoint).toHaveBeenCalledWith(expect.objectContaining({
+                route: '/models',
+                methods: ['GET']
+            }));
+        });
+
+        it('should attach models/details endpoint to router', () => {
+            // Setup
+            global.Endpoint = mockEndpoint;
+
+            // Execute
+            chatApiService.install_chat_endpoints_({ router: mockRouter });
+
+            // Verify
+            expect(mockEndpoint).toHaveBeenCalledWith(expect.objectContaining({
+                route: '/models/details',
+                methods: ['GET']
+            }));
+        });
+    });
+
+    describe('/models endpoint', () => {
+        it('should return list of models', async () => {
+            // Setup
+            global.Endpoint = mockEndpoint;
+            chatApiService.install_chat_endpoints_({ router: mockRouter });
+            
+            // Get the handler function
+            const handler = mockEndpoint.mock.calls[0][0].handler;
+            
+            // Execute
+            await handler(mockReq, mockRes);
+            
+            // Verify
+            expect(mockSUService.sudo).toHaveBeenCalled();
+            expect(mockRes.json).toHaveBeenCalledWith({ 
+                models: mockAIChatService.simple_model_list 
+            });
+        });
+    });
+
+    describe('/models/details endpoint', () => {
+        it('should return detailed list of models', async () => {
+            // Setup
+            global.Endpoint = mockEndpoint;
+            chatApiService.install_chat_endpoints_({ router: mockRouter });
+            
+            // Get the handler function
+            const handler = mockEndpoint.mock.calls[1][0].handler;
+            
+            // Execute
+            await handler(mockReq, mockRes);
+            
+            // Verify
+            expect(mockSUService.sudo).toHaveBeenCalled();
+            expect(mockRes.json).toHaveBeenCalledWith({ 
+                models: mockAIChatService.detail_model_list 
+            });
+        });
+    });
+});