Skip to content
Merged
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
28 changes: 28 additions & 0 deletions packages/ai-core/src/api-keys/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,34 @@ export async function billTextGenerationUsageToApiKey(
return priceInCent;
}

/**
* Bills speech (TTS) generation usage to the specified API key.
*
* @param apiKeyId - The unique identifier of the API key to bill
* @param modelId - The speech model used for generation
* @param costsInCent - The cost of the generation in cents
* @returns A promise that resolves to the price in cents charged for the operation
*/
export async function billSpeechGenerationUsageToApiKey({
apiKeyId,
modelId,
costsInCent,
}: {
apiKeyId: string;
modelId: string;
costsInCent: number;
}): Promise<number> {
await dbCreateCompletionUsage({
apiKeyId,
modelId,
completionTokens: 0,
promptTokens: 0,
totalTokens: 0,
costsInCent,
});
return costsInCent;
}

export async function isApiKeyOverQuota(apiKeyId: string): Promise<boolean> {
// Get the API key limit and sum usage costs since the start of the month
const [apiKeyData, completionCosts, imageCosts] = await Promise.all([
Expand Down
1 change: 1 addition & 0 deletions packages/ai-core/src/speech/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { billSpeechGenerationUsageToApiKey } from '../api-keys/billing';
export { generateSpeechById } from './generate';
export { streamSpeechById } from './generate-stream';
export {
Expand Down
2 changes: 2 additions & 0 deletions packages/api-database/src/functions/completion-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export async function dbCreateCompletionUsage(completionUsage: CompletionUsageIn
priceMetadata: modelData.priceMetadata,
promptTokens: completionUsage.promptTokens,
});
} else if (modelData.priceMetadata.type === 'speech') {
costsInCent = completionUsage.costsInCent ?? 0;
}

const insertedCompletionUsage = (
Expand Down
11 changes: 11 additions & 0 deletions packages/shared/src/speech/shared-chat-speech-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { billSpeechGenerationUsageToApiKey } from '@ais-chat/ai-core';
import { requireTeacherRole } from '@shared/auth/authorization-service';
import { UserModel } from '@shared/auth/user-model';
import { dbUpdateTokenUsageByCharacterChatId } from '@shared/db/functions/character';
import { dbGetFederalState } from '@shared/db/functions/federal-state';
import { dbUpdateTokenUsageBySharedLearningScenarioId } from '@shared/db/functions/learning-scenario';
import { ForbiddenError, NotFoundError } from '@shared/error';
import { FederalStateModel } from '@shared/federal-states/types';
Expand Down Expand Up @@ -167,6 +169,15 @@ function createSharedChatSpeechStrategy({
costsInCent,
});
}

const { apiKeyId } = await dbGetFederalState(context.federalState.id);
if (apiKeyId) {
await billSpeechGenerationUsageToApiKey({
apiKeyId,
modelId: model.id,
costsInCent,
});
}
},
reportUsage: async (_params, context, model, result) => {
const costsInCent = await result.costsInCent;
Expand Down
21 changes: 19 additions & 2 deletions packages/shared/src/speech/speech-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { generateSpeechById, MP3_MIME_TYPE, type SpeechMimeType } from '@ais-chat/ai-core';
import {
billSpeechGenerationUsageToApiKey,
generateSpeechById,
MP3_MIME_TYPE,
type SpeechMimeType,
} from '@ais-chat/ai-core';
import { UserModel } from '@shared/auth/user-model';
import { getConversation } from '@shared/conversation/conversation-service';
import { dbGetFederalState } from '@shared/db/functions/federal-state';
import { dbGetSpeechModel } from '@shared/db/functions/llm-model';
import { dbInsertConversationUsage } from '@shared/db/functions/token-usage';
import { LlmModelSelectModel } from '@shared/db/schema';
Expand Down Expand Up @@ -68,6 +74,8 @@ export async function generateChatSpeechStream({
model: LlmModelSelectModel,
result: ChatSpeechStreamResult,
) => {
const costsInCent = await result.costsInCent;

// we only store usage if there is an associated conversation
// not true for initial messages
if (context.conversation) {
Expand All @@ -77,7 +85,16 @@ export async function generateChatSpeechStream({
modelId: model.id,
completionTokens: 0,
promptTokens: 0,
costsInCent: await result.costsInCent,
costsInCent,
});
}

const { apiKeyId } = await dbGetFederalState(federalState.id);
Comment thread
recursive-hub marked this conversation as resolved.
Resolved
if (apiKeyId) {
await billSpeechGenerationUsageToApiKey({
apiKeyId,
modelId: model.id,
costsInCent,
});
}
Comment thread
recursive-hub marked this conversation as resolved.
Resolved
},
Expand Down
Loading