generated from nhcarrigan/template
feat: start logging token usage and subscriptions (#2)
Node.js CI / Lint and Test (push) Successful in 38s
Node.js CI / Lint and Test (push) Successful in 38s
### Explanation _No response_ ### Issue _No response_ ### Attestations - [x] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [x] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [x] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [x] I have pinned the dependencies to a specific patch version. ### Style - [x] I have run the linter and resolved any errors. - [x] My pull request uses an appropriate title, matching the conventional commit standards. - [x] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [x] All new and existing tests pass locally with my changes. - [x] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning Minor - My pull request introduces a new non-breaking feature. Reviewed-on: nhcarrigan/cordelia-taryne#2 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #2.
This commit is contained in:
+10
-2
@@ -12,7 +12,7 @@ import { proofread } from "./modules/proofread.js";
|
||||
import { query } from "./modules/query.js";
|
||||
import { summarise } from "./modules/summarise.js";
|
||||
import { instantiateServer } from "./server/serve.js";
|
||||
import { logHandler } from "./utils/logHandler.js";
|
||||
import { logger } from "./utils/logger.js";
|
||||
|
||||
const commands: Record<
|
||||
string,
|
||||
@@ -41,8 +41,16 @@ client.on(Events.InteractionCreate, (interaction) => {
|
||||
}
|
||||
});
|
||||
|
||||
client.on(Events.EntitlementCreate, (entitlement) => {
|
||||
void logger.log("info", `User ${entitlement.userId} has subscribed!`);
|
||||
});
|
||||
|
||||
client.on(Events.EntitlementDelete, (entitlement) => {
|
||||
void logger.log("info", `User ${entitlement.userId} has unsubscribed... :c`);
|
||||
});
|
||||
|
||||
client.on(Events.ClientReady, () => {
|
||||
logHandler.info("Bot is ready.");
|
||||
void logger.log("debug", "Bot is ready.");
|
||||
});
|
||||
|
||||
instantiateServer();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { personality } from "../config/personality.js";
|
||||
import { ai } from "../utils/ai.js";
|
||||
import { calculateCost } from "../utils/calculateCost.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
import type { ImageBlockParam } from "@anthropic-ai/sdk/resources/index.js";
|
||||
|
||||
@@ -109,4 +110,7 @@ export const alt = async(
|
||||
response?.text
|
||||
?? "I'm sorry, I don't have an answer for that. Please try again later.",
|
||||
);
|
||||
|
||||
const { usage } = messages;
|
||||
await calculateCost(usage, interaction.user.username, "alt-text");
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { personality } from "../config/personality.js";
|
||||
import { ai } from "../utils/ai.js";
|
||||
import { calculateCost } from "../utils/calculateCost.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
|
||||
/**
|
||||
@@ -40,4 +41,7 @@ export const evaluate = async(
|
||||
response?.text
|
||||
?? "I'm sorry, I don't have an answer for that. Please try again later.",
|
||||
);
|
||||
|
||||
const { usage } = messages;
|
||||
await calculateCost(usage, interaction.user.username, "evaluate");
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { personality } from "../config/personality.js";
|
||||
import { ai } from "../utils/ai.js";
|
||||
import { calculateCost } from "../utils/calculateCost.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
|
||||
/**
|
||||
@@ -41,4 +42,7 @@ export const mood = async(
|
||||
response?.text
|
||||
?? "I'm sorry, I don't have an answer for that. Please try again later.",
|
||||
);
|
||||
|
||||
const { usage } = messages;
|
||||
await calculateCost(usage, interaction.user.username, "mood");
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { personality } from "../config/personality.js";
|
||||
import { ai } from "../utils/ai.js";
|
||||
import { calculateCost } from "../utils/calculateCost.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
|
||||
/**
|
||||
@@ -41,4 +42,7 @@ export const proofread = async(
|
||||
response?.text
|
||||
?? "I'm sorry, I don't have an answer for that. Please try again later.",
|
||||
);
|
||||
|
||||
const { usage } = messages;
|
||||
await calculateCost(usage, interaction.user.username, "proofread");
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { personality } from "../config/personality.js";
|
||||
import { ai } from "../utils/ai.js";
|
||||
import { calculateCost } from "../utils/calculateCost.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
|
||||
/**
|
||||
@@ -41,4 +42,7 @@ export const query = async(
|
||||
response?.text
|
||||
?? "I'm sorry, I don't have an answer for that. Please try again later.",
|
||||
);
|
||||
|
||||
const { usage } = messages;
|
||||
await calculateCost(usage, interaction.user.username, "query");
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { personality } from "../config/personality.js";
|
||||
import { ai } from "../utils/ai.js";
|
||||
import { calculateCost } from "../utils/calculateCost.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
|
||||
/**
|
||||
@@ -41,4 +42,7 @@ export const summarise = async(
|
||||
response?.text
|
||||
?? "I'm sorry, I don't have an answer for that. Please try again later.",
|
||||
);
|
||||
|
||||
const { usage } = messages;
|
||||
await calculateCost(usage, interaction.user.username, "summarise");
|
||||
};
|
||||
|
||||
+8
-4
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import fastify from "fastify";
|
||||
import { logHandler } from "../utils/logHandler.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -60,12 +60,16 @@ export const instantiateServer = (): void => {
|
||||
|
||||
server.listen({ port: 5002 }, (error) => {
|
||||
if (error) {
|
||||
logHandler.error(error);
|
||||
void logger.error("instantiate server", error);
|
||||
return;
|
||||
}
|
||||
logHandler.info("Server listening on port 5002.");
|
||||
void logger.log("debug", "Server listening on port 5002.");
|
||||
});
|
||||
} catch (error) {
|
||||
logHandler.error(error);
|
||||
if (error instanceof Error) {
|
||||
void logger.error("instantiate server", error);
|
||||
return;
|
||||
}
|
||||
void logger.error("instantiate server", new Error("Unknown error"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { logger } from "./logger.js";
|
||||
import type { Usage } from "@anthropic-ai/sdk/resources/index.js";
|
||||
|
||||
/**
|
||||
* Calculates the cost of a command run by a user, and sends to
|
||||
* our logging service.
|
||||
* @param usage -- The usage payload from Anthropic.
|
||||
* @param uuid -- The Discord ID of the user who ran the command.
|
||||
* @param command -- The command that was run.
|
||||
*/
|
||||
export const calculateCost = async(
|
||||
usage: Usage,
|
||||
uuid: string,
|
||||
command: string,
|
||||
): Promise<void> => {
|
||||
const inputCost = usage.input_tokens * (3 / 1_000_000);
|
||||
const outputCost = usage.output_tokens * (15 / 1_000_000);
|
||||
const totalCost = inputCost + outputCost;
|
||||
await logger.log(
|
||||
"info",
|
||||
`User ${uuid} ran \`${command}\` which accepted ${usage.input_tokens.toString()} and generated ${usage.output_tokens.toString()}.
|
||||
|
||||
Total cost: ${totalCost.toLocaleString("en-GB", { currency: "USD", style: "currency" })}`,
|
||||
);
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { createLogger, format, transports, config } from "winston";
|
||||
|
||||
const { combine, timestamp, colorize, printf } = format;
|
||||
|
||||
/**
|
||||
* Standard log handler, using winston to wrap and format
|
||||
* messages. Call with `logHandler.log(level, message)`.
|
||||
* @param {string} level - The log level to use.
|
||||
* @param {string} message - The message to log.
|
||||
*/
|
||||
export const logHandler = createLogger({
|
||||
exitOnError: false,
|
||||
format: combine(
|
||||
timestamp({
|
||||
format: "YYYY-MM-DD HH:mm:ss",
|
||||
}),
|
||||
colorize(),
|
||||
printf((info) => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- Winston properties...
|
||||
return `${info.level}: ${info.timestamp}: ${info.message}`;
|
||||
}),
|
||||
),
|
||||
level: "silly",
|
||||
levels: config.npm.levels,
|
||||
transports: [ new transports.Console() ],
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { Logger } from "@nhcarrigan/logger";
|
||||
|
||||
export const logger = new Logger(
|
||||
"Cordelia Taryne",
|
||||
process.env.LOG_TOKEN ?? "",
|
||||
);
|
||||
Reference in New Issue
Block a user