mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-29 21:01:27 +00:00
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
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:
@@ -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 }) => {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user