Last reviewed: 2026-09-13
Direct answer
A reliable CometAPI Discord bot should acknowledge a slash command before it starts the model request. Call await interaction.deferReply() first, send the submitted prompt through a preconfigured CometAPI client, and finish with await interaction.editReply(...). Discord requires an initial response within three seconds, while a deferred response acknowledges the interaction and lets the application edit the original response later. Discord explains that lifecycle in Receiving and Responding to Interactions
, and the current discord.js interaction reference
exposes the methods and state properties used below.
This tutorial assumes you already have a server-side discord.js application with a logged-in Discord client and a command deployment routine. It adds one guild-tested /ask command. The command receives an authenticated CometAPI client through dependency injection, which keeps secret handling out of the command module and makes the failure path easy to test.
Install the required packages if your project does not already have them:
npm install discord.js openai
Configure the CometAPI client in your private runtime bootstrap by following the CometAPI quickstart
. That source documents the OpenAI-compatible base URL, server-side API-key storage, JavaScript client setup, current model selection, Chat Completions method, and assistant-text response path. Keep all secret values in your deployment secret store or local environment, never in this command file. If a screenshot or diagnostic record must show a secret field, replace the complete value with [REDACTED].
Create ask-command.js:
import { MessageFlags, SlashCommandBuilder } from "discord.js";
export const COMMAND_NAME = "ask";
export const data = new SlashCommandBuilder()
.setName(COMMAND_NAME)
.setDescription("Ask the configured CometAPI model")
.addStringOption((option) =>
option
.setName("prompt")
.setDescription("The question to send")
.setRequired(true)
.setMaxLength(1000),
);
const MAX_REPLY_CHARS = 1800;
const PUBLIC_FAILURE = "I could not complete that request. Please try again later.";
class EmptyModelResponseError extends Error {}
function classifyError(error) {
if (error instanceof EmptyModelResponseError) {
return "empty_model_response";
}
if (Number.isInteger(error?.status)) {
return "upstream_http_error";
}
return "request_error";
}
function safeWrite(writeLog, record) {
try {
writeLog(record);
} catch {
// Logging must not change the response shown to the user.
}
}
export async function execute(interaction, { ai, modelId, writeLog }) {
const startedAt = Date.now();
try {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!modelId) {
throw new Error("Missing model configuration");
}
const prompt = interaction.options.getString("prompt", true);
const completion = await ai.chat.completions.create({
model: modelId,
messages: [{ role: "user", content: prompt }],
});
const raw = completion.choices[0]?.message?.content?.trim();
if (!raw) {
throw new EmptyModelResponseError();
}
const shortened = raw.length > MAX_REPLY_CHARS;
const answer = shortened
? `${raw.slice(0, MAX_REPLY_CHARS)} [Response shortened by the bot.]`
: raw;
await interaction.editReply(answer);
safeWrite(writeLog, {
event: "discord_command",
outcome: "success",
command_name: interaction.commandName,
model_id: modelId,
elapsed_ms: Date.now() - startedAt,
reply_shortened: shortened,
reply_state: "edited",
});
} catch (error) {
const replyState = interaction.deferred
? "deferred"
: interaction.replied
? "replied"
: "unacknowledged";
safeWrite(writeLog, {
event: "discord_command",
outcome: "error",
command_name: interaction.commandName,
model_id: modelId ?? null,
elapsed_ms: Date.now() - startedAt,
error_class: classifyError(error),
status_code: Number.isInteger(error?.status) ? error.status : null,
reply_state: replyState,
});
try {
if (interaction.deferred || interaction.replied) {
await interaction.editReply(PUBLIC_FAILURE);
} else {
await interaction.reply({
content: PUBLIC_FAILURE,
flags: MessageFlags.Ephemeral,
});
}
} catch {
safeWrite(writeLog, {
event: "discord_error_reply",
outcome: "error",
command_name: interaction.commandName,
});
}
}
}
The 1,000-character prompt limit and 1,800-character reply limit are deliberate application policies, not claims about Discord’s maximum message size. They keep this first integration bounded and easy to test. The ephemeral deferral keeps the model response visible to the invoking user rather than turning every trial prompt into a channel message.
Pass data.toJSON() to your existing guild-command deployment routine. In the application bootstrap, route matching chat-input interactions to the module after both clients are ready:
import { Events } from "discord.js";
import { COMMAND_NAME, execute } from "./ask-command.js";
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName !== COMMAND_NAME) return;
await execute(interaction, {
ai: cometClient,
modelId: process.env.COMETAPI_MODEL,
writeLog: (record) => console.log(JSON.stringify(record)),
});
});
Here, client is the existing logged-in Discord client and cometClient is the authenticated OpenAI-compatible client created in the private bootstrap. The command module never needs to print, persist, or inspect either service credential.
Who this is for
This guide is for Node.js developers who already have a basic discord.js application and want to add a single AI-backed chat-input command. You should be comfortable deploying a command to a test server, supplying private runtime configuration, and running a persistent server process.
The implementation intentionally stops at one prompt and one answer. It does not add multi-turn memory, attachments, public channel responses, or a database. That narrow boundary makes Discord’s timing contract, CometAPI’s request contract, the user-visible failure response, and the logging policy easy to verify before the bot gains more capabilities.
Key takeaways
- Defer the Discord interaction before calling the external model service.
- Use
editReplyto replace the deferred response with either the answer or a generic failure message. - Keep the Discord credential and CometAPI API key in private server-side configuration.
- Select a current CometAPI model that supports the Chat Completions workflow.
- Keep command names, descriptions, and options within Discord’s documented application-command constraints.
- Log timing and normalized state metadata, not prompts, generated text, secrets, raw exception messages, stack traces, or interaction continuation values.
- Test the happy and error paths in a non-production Discord server before wider installation.
Sources checked
- The CometAPI quickstart supports the OpenAI-compatible client configuration, private API-key storage, current-model requirement, JavaScript Chat Completions call, and assistant-text access path used by the command.
- Discord’s Receiving and Responding to Interactions supports the three-second initial-response requirement, deferred-response lifecycle, 15-minute interaction continuation window, and later editing of the original response.
- Discord’s Application Commands documentation defines chat-input commands and the constraints for command names, descriptions, options, required fields, and string-option lengths.
- The ChatInputCommandInteraction class reference
confirms the current discord.js surface used here, including
isChatInputCommand,deferReply,editReply,reply,deferred, andreplied.
Contract details to verify
Command registration
The deployed command and the runtime handler must agree on the command name and option names. This example registers ask with a required string option named prompt. Discord documents chat-input command and option names as 1 to 32 characters and requires lowercase variants where they exist. It documents descriptions as 1 to 100 characters and permits a configured maximum length of up to 6,000 characters for a string option. The sample’s names and its smaller 1,000-character input policy fit inside those constraints.
Start with a guild-scoped command in a test server so you can verify the complete flow in a controlled place. Whenever the builder changes, redeploy the command definition before testing the handler. If Discord shows an old option list, compare the deployed definition with data.toJSON() rather than changing the handler blindly.
Response timing
Discord requires the initial interaction response within three seconds. In this design, deferReply is the first awaited operation after command routing. Do not perform the model call, database work, remote feature-flag lookup, or slow logging before it.
Discord also documents that interaction continuation values remain usable for 15 minutes. That longer window enables a later edit, but it does not extend the initial three-second deadline. Set an application timeout comfortably inside the later window so a stalled upstream request does not leave a perpetual loading response and then lose the ability to edit it.
CometAPI request and response
The injected ai dependency must be the OpenAI-compatible client configured for CometAPI in the private bootstrap. The quickstart shows the JavaScript chat.completions.create call with a model and messages array, then reads assistant text from completion.choices[0].message.content. This command follows that documented shape.
The model identifier belongs in deployment configuration because it must be current and appropriate for Chat Completions. Validate it during deployment rather than copying an old model name into the command module. An empty or whitespace-only assistant message is treated as an application error so the user gets a definite response instead of an apparently successful blank edit.
Happy-path operator workflow
- Configure both authenticated clients in the private runtime bootstrap and set a current chat-capable CometAPI model identifier.
- Deploy
data.toJSON()as a guild command in a non-production Discord server. - Start the bot and confirm its ordinary readiness signal without printing configuration values.
- Invoke
/askwith a short, non-sensitive question. - Confirm Discord shows the deferred state promptly and then replaces it with an ephemeral answer.
- Inspect the
discord_commandrecord. It should showoutcomeassuccess,reply_stateasedited, a plausibleelapsed_ms, and whether the reply was shortened. - Confirm the log contains neither the submitted question nor the generated answer.
Error-path operator workflow
Use dependency injection to test failure without altering a real credential. In the staging runtime only, replace cometClient with a test double whose chat.completions.create method throws a synthetic error. Invoke /ask again and verify that the interaction is deferred first, the loading state becomes the generic failure message, and the audit event records outcome as error.
For a second controlled case, make the test double return a response whose assistant content is empty. The handler should classify it as empty_model_response and show the same generic message. Restore the real configured client after both tests. These checks exercise the user-visible recovery behavior without sending prompt data to logs or deliberately damaging secret configuration.
Sanitized logging fields
The allowlist in the example contains event, outcome, command_name, model_id, elapsed_ms, reply_shortened, error_class, status_code, and reply_state. Those fields answer the routine operational questions: which command ran, which configured model path it used, how long it took, whether output was shortened, and where the response lifecycle stopped.
Do not add prompt text, generated content, service credentials, raw error messages, stack traces, or Discord interaction continuation values to general application logs. Even normalized metadata should have access controls and a retention period appropriate to the bot’s environment.
Failure modes
The handler defers too late. Any slow work before deferReply can consume Discord’s initial three-second window. Keep configuration validation at process startup and make deferral the first awaited operation after the command-name checks.
The deferred response expires before editing. Discord’s 15-minute interaction window is finite. A model request that hangs indefinitely can outlive it. Add an application-side timeout and attempt the generic edit while the interaction is still usable.
The command definition and handler drift apart. Discord may deliver an interaction with options that do not match the local assumptions if the registered command was not redeployed. Compare the registered name and options with the builder output, then redeploy through the normal command pipeline.
The model identifier is missing, stale, or incompatible. The CometAPI quickstart requires a current model appropriate to the chosen API. Keep the model identifier in configuration, validate it before release, and use the generic error path if the upstream rejects it.
The CometAPI client points at the wrong service configuration. A mismatched base URL or unavailable server-side key prevents the request from succeeding. Verify the bootstrap against the CometAPI quickstart and report only a normalized error category to general logs.
The code attempts a second initial reply. Once an interaction is deferred, use editReply, not another initial reply. The recovery branch checks deferred and replied before choosing how to show the failure message.
The response has no assistant text. Optional access protects against a property-access crash, while EmptyModelResponseError turns a blank result into an explicit, observable failure.
The generated answer exceeds the application’s reply policy. The sample shortens text beyond 1,800 characters and labels the result. A production bot can choose a different policy, such as an approved follow-up flow, but it should define and test that behavior rather than silently dropping content.
Logging leaks user content. Raw prompts, completions, exception messages, and stack traces can contain sensitive material. Use an allowlist, test the emitted JSON, and keep deeper diagnostics in a separately controlled process when they are genuinely needed.
Even the error response fails. If the interaction can no longer be edited or acknowledged, the nested recovery emits discord_error_reply. Alert on that event because the user may otherwise see only Discord’s failed-interaction state.
FAQ
Why defer instead of waiting for CometAPI and then replying?
The external request may take longer than Discord’s three-second initial-response deadline. Deferring promptly acknowledges the command; editReply replaces that deferred response once model work finishes.
Why is the response ephemeral?
The current discord.js reference documents ephemeral deferral through MessageFlags.Ephemeral. It is a sensible default for testing prompts because the result is directed to the invoking user. Remove or change that policy only after deciding how public prompts and model output should be handled in your server.
Why inject the CometAPI client?
Injection keeps authenticated client setup in one private bootstrap module, prevents the command from handling secret values, and lets an operator substitute predictable success and failure doubles in staging. The command still uses the request and response shape documented by CometAPI.
Is 1,000 characters Discord’s prompt maximum?
No. It is this tutorial’s application policy. Discord documents string-option maximum lengths up to 6,000 characters. A smaller limit makes the initial bot easier to observe and reduces unexpectedly large requests while you validate the integration.
Should prompt text be included in an error log?
No. The sanitized fields are enough to distinguish success, an upstream HTTP failure, an empty model response, and a failed Discord edit. Prompt and completion bodies add exposure without being necessary for routine health monitoring.
What if the model returns a long answer?
This sample shortens it using its own 1,800-character policy and tells the user that shortening occurred. If your product needs full answers, design a reviewed follow-up or storage flow and test it separately.
What does discord_error_reply mean?
It means the handler caught the original failure but could not send or edit the user-facing error response. Common areas to inspect are whether the first acknowledgement was late, whether the interaction window elapsed, and whether Discord was reachable at the time.
Reader next step
Before connecting the command to a wider server, smoke-test the CometAPI Chat Completions contract with the same model configuration. Then review how to keep CometAPI keys out of repositories .
After both controlled workflows pass, add client-side timeout handling
, run the bot under your normal process supervisor, and alert on discord_error_reply events.