Skip to content

[docs]: clarify CostService usage and improve metered services discoverability #1285

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/backend/doc/features/metered-services.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Metered Services in Puter

Puter implements metered services through a centralized cost tracking and credit management system. This document describes the core mechanisms that enable metered services in Puter's open-source codebase.

## Overview

Metered services in Puter are managed through the `CostService`, which provides a unified interface for:

- Checking available credits
- Recording service costs
- Tracking funding updates

While the specific funding logic and credit allocation may vary in different Puter deployments (e.g., puter.com), the underlying mechanism remains consistent.

## Core Components

### CostService

Location: `src/backend/src/services/drivers/CostService.js`

The CostService is the central component for metered services, providing the following key functionalities:

1. **Credit Availability Check**

```javascript
async get_funding_allowed(options = { minimum: 100 })
```

- Verifies if sufficient credits are available for an operation
- Default minimum threshold is 100 (1/10th of a cent)
- Returns boolean indicating if funding is allowed

2. **Cost Recording**

```javascript
async record_cost({ cost })
```

- Records the cost of an operation
- Associates costs with the current actor
- Emits events for credit tracking

3. **Funding Updates**
```javascript
async record_funding_update({ old_amount, new_amount })
```
- Tracks changes in user funding
- Maintains audit trail of funding modifications

## Event System

CostService uses an event-based architecture to communicate with other system components:

- `credit.check-available`: Checks available credits
- `credit.record-cost`: Records operation costs
- `credit.funding-update`: Tracks funding changes

## Integration

### Using CostService in Modules

To integrate metered services in a module:

1. Access the service:

```javascript
const costService = services.get("cost");
```

2. Check funding before expensive operations:

```javascript
const fundingAllowed = await costService.get_funding_allowed({
minimum: requiredAmount,
});
if (!fundingAllowed) {
throw new Error("Insufficient credits");
}
```

3. Record costs after operations:
```javascript
await costService.record_cost({ cost: operationCost });
```

## Security Considerations

- All cost operations are associated with the current actor
- Cost recording includes audit logging
- Minimum thresholds prevent micro-transactions

## Related Documentation

- For AI-specific metering, see: [PuterAI Documentation](../modules/puterai/README.md)
- For driver implementation details: [How to Make a Driver](../howto_make_driver.md)
16 changes: 6 additions & 10 deletions src/backend/src/modules/puterai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Services are conditionally registered based on configuration settings, allowing
flexible deployment with different AI providers like AWS, OpenAI, Claude, Together AI,
Mistral, Groq, and XAI.

## Metered Services

This module makes use of Puter's metered services system for tracking and managing costs of AI operations. For detailed information about how metered services work in Puter, including cost tracking, credit management, and integration details, please see the [Metered Services Documentation](../../doc/features/metered-services.md).

## Services

### AIChatService
Expand All @@ -26,6 +30,7 @@ and populating model lists/maps from providers.

Registers each provider as an 'ai-chat' service alias and fetches their
available models and pricing information. Populates:

- simple_model_list: Basic list of supported models
- detail_model_list: Detailed model info including costs
- detail_model_map: Maps model IDs/aliases to their details
Expand All @@ -34,8 +39,6 @@ available models and pricing information. Populates:

##### `register_provider`



##### `moderate`

Moderates chat messages for inappropriate content using OpenAI's moderation service
Expand Down Expand Up @@ -63,8 +66,6 @@ the first one that is not in the tried list.

##### `get_model_from_request`



### AIInterfaceService

Service class that manages AI interface registrations and configurations.
Expand Down Expand Up @@ -142,8 +143,6 @@ Service that emulates Claude's behavior using alternative AI models

##### `adapt_model`



### ClaudeService

ClaudeService class extends BaseService to provide integration with Anthropic's Claude AI models.
Expand Down Expand Up @@ -240,8 +239,6 @@ validation, and spending tracking.

##### `generate`



### TogetherAIService

TogetherAIService class provides integration with Together AI's language models.
Expand Down Expand Up @@ -270,8 +267,6 @@ Gets the system prompt used for AI interactions

##### `adapt_model`



##### `get_default_model`

Returns the default model identifier for the XAI service
Expand All @@ -285,6 +280,7 @@ removed it may become possible to move this module to an
extension.

**Imports:**

- `../../api/APIError`
- `../../services/auth/PermissionService`
- `../../services/BaseService` (use.BaseService)
Expand Down
180 changes: 113 additions & 67 deletions src/gui/src/helpers/update_username_in_gui.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,84 +7,130 @@
* 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 update_username_in_gui = function(new_username){
// ------------------------------------------------------------
// Update all item/window/... paths, with the new username
// ------------------------------------------------------------
$(':not([data-path=""]),:not([data-item-path=""])').each((i, el)=>{
const $el = $(el);
const attr_path = $el.attr('data-path');
const attr_item_path = $el.attr('data-item-path');
const attr_shortcut_to_path = $el.attr('data-shortcut_to_path');
// data-path
if(attr_path && attr_path !== 'null' && attr_path !== 'undefined'){
// /[username]
if(attr_path === '/' + window.user.username)
$el.attr('data-path', '/' + new_username);
// /[username]/...
else if (attr_path.startsWith('/' + window.user.username + '/'))
$el.attr('data-path', attr_path.replace('/' + window.user.username + '/', '/' + new_username + '/'));
const update_username_in_gui = function (
new_username,
old_username = window.user.username
) {
// ------------------------------------------------------------
// Update all item/window/... paths, with the new username
// ------------------------------------------------------------
$(':not([data-path=""]),:not([data-item-path=""])').each((i, el) => {
const $el = $(el);
const attr_path = $el.attr("data-path");
const attr_item_path = $el.attr("data-item-path");
const attr_shortcut_to_path = $el.attr("data-shortcut_to_path");
// data-path
if (attr_path && attr_path !== "null" && attr_path !== "undefined") {
// /[username]
if (attr_path === "/" + old_username)
$el.attr("data-path", "/" + new_username);
// /[username]/...
else if (attr_path.startsWith("/" + old_username + "/"))
$el.attr(
"data-path",
attr_path.replace("/" + old_username + "/", "/" + new_username + "/")
);

// .window-navbar-path-dirname
if($el.hasClass('window-navbar-path-dirname') && attr_path === '/' + window.user.username)
$el.text(new_username)
// .window-navbar-path-dirname
if (
$el.hasClass("window-navbar-path-dirname") &&
attr_path === "/" + old_username
)
$el.text(new_username);
// .window-navbar-path-input value
else if ($el.hasClass("window-navbar-path-input")) {
// /[username]
if (attr_path === "/" + old_username) $el.val("/" + new_username);
// /[username]/...
else if (attr_path.startsWith("/" + old_username + "/"))
$el.val(
attr_path.replace(
"/" + old_username + "/",
"/" + new_username + "/"
)
);
}

// .window-navbar-path-input value
else if($el.hasClass('window-navbar-path-input')){
// /[username]
if(attr_path === '/' + window.user.username)
$el.val('/' + new_username);
// /[username]/...
else if (attr_path.startsWith('/' + window.user.username + '/'))
$el.val(attr_path.replace('/' + window.user.username + '/', '/' + new_username + '/'));
}
}
// data-shortcut_to_path
if(attr_shortcut_to_path && attr_shortcut_to_path !== '' && attr_shortcut_to_path !== 'null' && attr_shortcut_to_path !== 'undefined'){
// home dir
if(attr_shortcut_to_path === '/' + window.user.username)
$el.attr('data-shortcut_to_path', '/' + new_username);
// every other paths
else if(attr_shortcut_to_path.startsWith('/' + window.user.username + '/'))
$el.attr('data-shortcut_to_path', attr_shortcut_to_path.replace('/' + window.user.username + '/', '/' + new_username + '/'));
// Update sidebar shared user paths
if (
$el.hasClass("window-sidebar-item") &&
attr_path === "/" + old_username
) {
$el.attr("data-path", "/" + new_username);
// Also update the display text if this is a shared user item
if ($el.closest(".shared-users-list").length) {
$el.text(new_username);
}
// data-item-path
if(attr_item_path && attr_item_path !== 'null' && attr_item_path !== 'undefined'){
// /[username]
if(attr_item_path === '/' + window.user.username)
$el.attr('data-item-path', '/' + new_username);
// /[username]/...
else if (attr_item_path.startsWith('/' + window.user.username + '/'))
$el.attr('data-item-path', attr_item_path.replace('/' + window.user.username + '/', '/' + new_username + '/'));
}

// any element with username class
$('.username').text(new_username);
})
}
}
// data-shortcut_to_path
if (
attr_shortcut_to_path &&
attr_shortcut_to_path !== "" &&
attr_shortcut_to_path !== "null" &&
attr_shortcut_to_path !== "undefined"
) {
// home dir
if (attr_shortcut_to_path === "/" + old_username)
$el.attr("data-shortcut_to_path", "/" + new_username);
// every other paths
else if (attr_shortcut_to_path.startsWith("/" + old_username + "/"))
$el.attr(
"data-shortcut_to_path",
attr_shortcut_to_path.replace(
"/" + old_username + "/",
"/" + new_username + "/"
)
);
}
// data-item-path
if (
attr_item_path &&
attr_item_path !== "null" &&
attr_item_path !== "undefined"
) {
// /[username]
if (attr_item_path === "/" + old_username)
$el.attr("data-item-path", "/" + new_username);
// /[username]/...
else if (attr_item_path.startsWith("/" + old_username + "/"))
$el.attr(
"data-item-path",
attr_item_path.replace(
"/" + old_username + "/",
"/" + new_username + "/"
)
);
}

// todo update all window paths
$('.window').each((i, el)=>{
})
// any element with username class
if (old_username === window.user.username) {
$(".username").text(new_username);
}
});

window.desktop_path = '/' + new_username + '/Desktop';
window.trash_path = '/' + new_username + '/Trash';
window.appdata_path = '/' + new_username + '/AppData';
window.docs_path = '/' + new_username + '/Documents';
window.pictures_path = '/' + new_username + '/Pictures';
window.videos_path = '/' + new_username + '/Videos';
window.desktop_path = '/' + new_username + '/Desktop';
window.public_path = '/' + new_username + '/Public';
window.home_path = '/' + new_username;
}
// Update window paths if this is the current user
if (old_username === window.user.username) {
window.desktop_path = "/" + new_username + "/Desktop";
window.trash_path = "/" + new_username + "/Trash";
window.appdata_path = "/" + new_username + "/AppData";
window.docs_path = "/" + new_username + "/Documents";
window.pictures_path = "/" + new_username + "/Pictures";
window.videos_path = "/" + new_username + "/Videos";
window.desktop_path = "/" + new_username + "/Desktop";
window.public_path = "/" + new_username + "/Public";
window.home_path = "/" + new_username;
}
};

export default update_username_in_gui;
export default update_username_in_gui;