diff --git a/extensions/api.d.ts b/extensions/api.d.ts index 2e0216f3e..8f6d304c5 100644 --- a/extensions/api.d.ts +++ b/extensions/api.d.ts @@ -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'; diff --git a/src/backend/src/services/MeteringService/MeteringService.ts b/src/backend/src/services/MeteringService/MeteringService.ts index 4a56ca991..9d43a9b3b 100644 --- a/src/backend/src/services/MeteringService/MeteringService.ts +++ b/src/backend/src/services/MeteringService/MeteringService.ts @@ -27,11 +27,14 @@ export class MeteringService { } utilRecordUsageObject>(trackedUsageObject: T, actor: Actor, modelPrefix: string, costsOverrides?: Partial>) { - 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 () { diff --git a/src/backend/src/services/ai/chat/AIChatService.ts b/src/backend/src/services/ai/chat/AIChatService.ts index 8796e964c..5c57cb77d 100644 --- a/src/backend/src/services/ai/chat/AIChatService.ts +++ b/src/backend/src/services/ai/chat/AIChatService.ts @@ -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 => { diff --git a/src/backend/src/services/ai/chat/providers/MistralAiProvider/MistralAiProvider.ts b/src/backend/src/services/ai/chat/providers/MistralAiProvider/MistralAiProvider.ts index f676a7a42..9c9d870d7 100644 --- a/src/backend/src/services/ai/chat/providers/MistralAiProvider/MistralAiProvider.ts +++ b/src/backend/src/services/ai/chat/providers/MistralAiProvider/MistralAiProvider.ts @@ -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; }, }); diff --git a/src/backend/src/services/ai/chat/providers/MistralAiProvider/models.ts b/src/backend/src/services/ai/chat/providers/MistralAiProvider/models.ts index 9f3029001..5e70bbbdf 100644 --- a/src/backend/src/services/ai/chat/providers/MistralAiProvider/models.ts +++ b/src/backend/src/services/ai/chat/providers/MistralAiProvider/models.ts @@ -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: [ diff --git a/src/backend/src/services/ai/chat/providers/OllamaProvider.ts b/src/backend/src/services/ai/chat/providers/OllamaProvider.ts index e38bdc9d2..f324a2c9c 100644 --- a/src/backend/src/services/ai/chat/providers/OllamaProvider.ts +++ b/src/backend/src/services/ai/chat/providers/OllamaProvider.ts @@ -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'; } -} \ No newline at end of file +} diff --git a/src/backend/src/services/ai/video/TogetherVideoGenerationService/TogetherVideoGenerationService.js b/src/backend/src/services/ai/video/TogetherVideoGenerationService/TogetherVideoGenerationService.js index b0f8c3177..87b8a38d9 100644 --- a/src/backend/src/services/ai/video/TogetherVideoGenerationService/TogetherVideoGenerationService.js +++ b/src/backend/src/services/ai/video/TogetherVideoGenerationService/TogetherVideoGenerationService.js @@ -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; }