feat: add public endpoint for models list (#1228)
Docker Image CI / build-and-push-image (push) Waiting to run
Maintain Release Merge PR / update-release-pr (push) Waiting to run
release-please / release-please (push) Waiting to run
test / test (18.x) (push) Waiting to run
test / test (20.x) (push) Waiting to run
test / test (22.x) (push) Waiting to run

* 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
This commit is contained in:
Eric Dubé
2025-03-27 17:12:09 -04:00
committed by GitHub
parent 39048a9e2e
commit 45c072ff93
3 changed files with 284 additions and 0 deletions
+3
View File
@@ -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
View File
@@ -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,
};
@@ -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
});
});
});
});