mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-03 16:10:31 +00:00
fix: ai metering (#2393)
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled
* fix: expose getUserService in extension typings * fix: ai metering
This commit is contained in:
Vendored
+1
-1
@@ -3,9 +3,9 @@ import type { WebServerService } from '@heyputer/backend/src/modules/web/WebServ
|
||||
import type query from '@heyputer/backend/src/om/query/query';
|
||||
import type { Actor } from '@heyputer/backend/src/services/auth/Actor.js';
|
||||
import type { BaseDatabaseAccessService } from '@heyputer/backend/src/services/database/BaseDatabaseAccessService.d.ts';
|
||||
import type { GetUserService } from '@heyputer/backend/src/services/GetUserService.js';
|
||||
import type { EmailService } from '@heyputer/backend/src/services/EmailService.js';
|
||||
import type { EntityStoreService } from '@heyputer/backend/src/services/EntityStoreService.js';
|
||||
import type { GetUserService } from '@heyputer/backend/src/services/GetUserService.js';
|
||||
import type { MeteringService } from '@heyputer/backend/src/services/MeteringService/MeteringService.ts';
|
||||
import type { MeteringServiceWrapper } from '@heyputer/backend/src/services/MeteringService/MeteringServiceWrapper.mjs';
|
||||
import type { DynamoKVStore } from '@heyputer/backend/src/services/repositories/DynamoKVStore/DynamoKVStore.ts';
|
||||
|
||||
@@ -27,11 +27,14 @@ export class MeteringService {
|
||||
}
|
||||
|
||||
utilRecordUsageObject<T extends Record<string, number>>(trackedUsageObject: T, actor: Actor, modelPrefix: string, costsOverrides?: Partial<Record<keyof T, number>>) {
|
||||
this.batchIncrementUsages(actor, Object.entries(trackedUsageObject).map(([usageKind, amount]) => ({
|
||||
usageType: `${modelPrefix}:${usageKind}`,
|
||||
usageAmount: amount,
|
||||
costOverride: costsOverrides?.[usageKind as keyof T] || undefined,
|
||||
})));
|
||||
this.batchIncrementUsages(actor, Object.entries(trackedUsageObject).map(([usageKind, amount]) => {
|
||||
const hasOverride = !!costsOverrides && Object.prototype.hasOwnProperty.call(costsOverrides, usageKind);
|
||||
return {
|
||||
usageType: `${modelPrefix}:${usageKind}`,
|
||||
usageAmount: amount,
|
||||
costOverride: hasOverride ? costsOverrides![usageKind as keyof T] : undefined,
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
#getMonthYearString () {
|
||||
|
||||
@@ -454,14 +454,6 @@ export class AIChatService extends BaseService {
|
||||
|
||||
const fallback = this.getFallbackModel(model.id, tried, triedProviders);
|
||||
|
||||
tried.push(model.id);
|
||||
triedProviders.push(model.provider!);
|
||||
|
||||
if ( tried.length >= MAX_FALLBACKS ) {
|
||||
console.error('max fallbacks reached', { tried, triedProviders });
|
||||
break;
|
||||
}
|
||||
|
||||
if ( ! fallback ) {
|
||||
throw new Error('no fallback model available');
|
||||
}
|
||||
@@ -478,6 +470,14 @@ export class AIChatService extends BaseService {
|
||||
|
||||
let fallBackModel = this.getModel({ modelId: fallbackModelId, provider: fallbackProvider });
|
||||
|
||||
tried.push(fallbackModelId);
|
||||
triedProviders.push(fallbackProvider);
|
||||
|
||||
if ( tried.length > MAX_FALLBACKS ) {
|
||||
console.error('max fallbacks reached', { tried, triedProviders });
|
||||
break;
|
||||
}
|
||||
|
||||
const fallbackUsageAllowed = await this.meteringService.hasEnoughCredits(actor, 1); // we checked earlier, assume same costs
|
||||
|
||||
if ( ! fallbackUsageAllowed ) {
|
||||
@@ -655,7 +655,7 @@ export class AIChatService extends BaseService {
|
||||
if ( targetModel.id.startsWith('openrouter:') || targetModel.id.startsWith('togetherai:') ) {
|
||||
[aiProvider, modelToSearch] = targetModel.id.replace('openrouter:', '').replace('togetherai:', '').toLowerCase().split('/');
|
||||
} else {
|
||||
[aiProvider, modelToSearch] = targetModel.provider!.toLowerCase().replace('gemini', 'google').replace('openai-completion', 'openai'), targetModel.id.toLowerCase();
|
||||
[aiProvider, modelToSearch] = targetModel.provider!.toLowerCase().replace('gemini', 'google').replace('openai-completion', 'openai').replace('openai-responses', 'openai'), targetModel.id.toLowerCase();
|
||||
}
|
||||
|
||||
const potentialMatches = models.filter(model => {
|
||||
|
||||
@@ -107,9 +107,10 @@ export class MistralAIProvider implements IChatProvider {
|
||||
stream,
|
||||
usage_calculator: ({ usage }) => {
|
||||
const trackedUsage = OpenAIUtil.extractMeteredUsage(usage);
|
||||
this.#meteringService.utilRecordUsageObject(trackedUsage, actor, `mistral:${selectedModel.id}`);
|
||||
// Still return legacy cost calculation for compatibility
|
||||
|
||||
const costsOverrideFromModel = Object.fromEntries(Object.entries(trackedUsage).map(([k, v]) => {
|
||||
return [k, v * (selectedModel.costs[k] || 0)];
|
||||
}));
|
||||
this.#meteringService.utilRecordUsageObject(trackedUsage, actor, `mistral:${selectedModel.id}`, costsOverrideFromModel);
|
||||
return trackedUsage;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { IChatModel } from '../types';
|
||||
|
||||
export const MISTRAL_MODELS: IChatModel[] = [
|
||||
{
|
||||
puterId: "mistralai:mistralai/mistral-medium-2508",
|
||||
puterId: 'mistralai:mistralai/mistral-medium-2508',
|
||||
id: 'mistral-medium-2508',
|
||||
name: 'mistral-medium-2508',
|
||||
aliases: [
|
||||
@@ -23,7 +23,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/open-mistral-7b",
|
||||
puterId: 'mistralai:mistralai/open-mistral-7b',
|
||||
id: 'open-mistral-7b',
|
||||
name: 'open-mistral-7b',
|
||||
aliases: [
|
||||
@@ -44,7 +44,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/open-mistral-nemo",
|
||||
puterId: 'mistralai:mistralai/open-mistral-nemo',
|
||||
id: 'open-mistral-nemo',
|
||||
name: 'open-mistral-nemo',
|
||||
aliases: [
|
||||
@@ -66,7 +66,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/pixtral-large-2411",
|
||||
puterId: 'mistralai:mistralai/pixtral-large-2411',
|
||||
id: 'pixtral-large-2411',
|
||||
name: 'pixtral-large-2411',
|
||||
aliases: [
|
||||
@@ -87,7 +87,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/codestral-2508",
|
||||
puterId: 'mistralai:mistralai/codestral-2508',
|
||||
id: 'codestral-2508',
|
||||
name: 'codestral-2508',
|
||||
aliases: [
|
||||
@@ -107,7 +107,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/devstral-small-2507",
|
||||
puterId: 'mistralai:mistralai/devstral-small-2507',
|
||||
id: 'devstral-small-2507',
|
||||
name: 'devstral-small-2507',
|
||||
aliases: [
|
||||
@@ -128,7 +128,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/devstral-medium-2507",
|
||||
puterId: 'mistralai:mistralai/devstral-medium-2507',
|
||||
id: 'devstral-medium-2507',
|
||||
name: 'devstral-medium-2507',
|
||||
aliases: [
|
||||
@@ -149,7 +149,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/mistral-small-2506",
|
||||
puterId: 'mistralai:mistralai/mistral-small-2506',
|
||||
id: 'mistral-small-2506',
|
||||
name: 'mistral-small-2506',
|
||||
aliases: [
|
||||
@@ -169,7 +169,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/magistral-medium-2509",
|
||||
puterId: 'mistralai:mistralai/magistral-medium-2509',
|
||||
id: 'magistral-medium-2509',
|
||||
name: 'magistral-medium-2509',
|
||||
aliases: [
|
||||
@@ -189,7 +189,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/magistral-small-2509",
|
||||
puterId: 'mistralai:mistralai/magistral-small-2509',
|
||||
id: 'magistral-small-2509',
|
||||
name: 'magistral-small-2509',
|
||||
aliases: [
|
||||
@@ -209,7 +209,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/voxtral-mini-2507",
|
||||
puterId: 'mistralai:mistralai/voxtral-mini-2507',
|
||||
id: 'voxtral-mini-2507',
|
||||
name: 'voxtral-mini-2507',
|
||||
aliases: [
|
||||
@@ -229,7 +229,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/voxtral-small-2507",
|
||||
puterId: 'mistralai:mistralai/voxtral-small-2507',
|
||||
id: 'voxtral-small-2507',
|
||||
name: 'voxtral-small-2507',
|
||||
aliases: [
|
||||
@@ -249,7 +249,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/mistral-large-2512",
|
||||
puterId: 'mistralai:mistralai/mistral-large-2512',
|
||||
id: 'mistral-large-latest',
|
||||
name: 'mistral-large-2512',
|
||||
aliases: [
|
||||
@@ -269,7 +269,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/ministral-3b-2512",
|
||||
puterId: 'mistralai:mistralai/ministral-3b-2512',
|
||||
id: 'ministral-3b-2512',
|
||||
name: 'ministral-3b-2512',
|
||||
aliases: [
|
||||
@@ -289,7 +289,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/ministral-8b-2512",
|
||||
puterId: 'mistralai:mistralai/ministral-8b-2512',
|
||||
id: 'ministral-8b-2512',
|
||||
name: 'ministral-8b-2512',
|
||||
aliases: [
|
||||
@@ -309,7 +309,7 @@ export const MISTRAL_MODELS: IChatModel[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
puterId: "mistralai:mistralai/ministral-14b-2512",
|
||||
puterId: 'mistralai:mistralai/ministral-14b-2512',
|
||||
id: 'ministral-14b-2512',
|
||||
name: 'ministral-14b-2512',
|
||||
aliases: [
|
||||
|
||||
@@ -125,6 +125,7 @@ export class OllamaChatProvider implements IChatProvider {
|
||||
} as ChatCompletionCreateParams) ;
|
||||
|
||||
const modelDetails = (await this.models()).find(m => m.id === `ollama:${model}`);
|
||||
const modelIdForMetering = modelDetails?.id ?? (model ? (model.startsWith('ollama/') ? `ollama:${model}` : `ollama:ollama/${model}`) : undefined);
|
||||
return OpenAIUtil.handle_completion_output({
|
||||
usage_calculator: ({ usage }) => {
|
||||
|
||||
@@ -136,7 +137,9 @@ export class OllamaChatProvider implements IChatProvider {
|
||||
const costOverwrites = Object.fromEntries(Object.keys(trackedUsage).map((k) => {
|
||||
return [k, 0]; // override to 0 since local is free
|
||||
}));
|
||||
this.#meteringService.utilRecordUsageObject(trackedUsage, actor, modelDetails!.id, costOverwrites);
|
||||
if ( modelIdForMetering ) {
|
||||
this.#meteringService.utilRecordUsageObject(trackedUsage, actor, modelIdForMetering, costOverwrites);
|
||||
}
|
||||
return trackedUsage;
|
||||
},
|
||||
stream,
|
||||
@@ -154,4 +157,4 @@ export class OllamaChatProvider implements IChatProvider {
|
||||
getDefaultModel () {
|
||||
return 'gpt-oss:20b';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ const POLL_INTERVAL_MS = 5_000;
|
||||
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
||||
const DEFAULT_MODEL = 'minimax/video-01-director';
|
||||
const DEFAULT_DURATION_SECONDS = 6;
|
||||
const DEFAULT_USAGE_KEY = 'togetherai:default';
|
||||
const DEFAULT_USAGE_KEY = 'together-video:default';
|
||||
|
||||
let models = [];
|
||||
|
||||
@@ -253,7 +253,7 @@ class TogetherVideoGenerationService extends BaseService {
|
||||
|
||||
#determineUsageKey (model) {
|
||||
if ( typeof model === 'string' && model.trim() ) {
|
||||
return `togetherai:${model}`;
|
||||
return `together-video:${model}`;
|
||||
}
|
||||
return DEFAULT_USAGE_KEY;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user