mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-03-02 15:29:16 +00:00
Adds a comprehensive quota management system for LLM models that allows: - Setting per-group (applied per user in the group) token and usage limits with configurable durations - Tracking and enforcing token/usage limits across user groups - Quota reset periods (hourly, daily, weekly, or custom) - Admin UI for managing quotas with real-time updates This system provides granular control over LLM API usage by allowing admins to define limits on both total tokens and number of requests per group. Supports multiple concurrent quotas per model and automatically handles quota resets. Co-authored-by: Keegan George <kgeorge13@gmail.com>
262 lines
7.4 KiB
Plaintext
262 lines
7.4 KiB
Plaintext
import Component from "@glimmer/component";
|
|
import { tracked } from "@glimmer/tracking";
|
|
import { fn } from "@ember/helper";
|
|
import { on } from "@ember/modifier";
|
|
import { action } from "@ember/object";
|
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
|
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
|
import { service } from "@ember/service";
|
|
import AceEditor from "discourse/components/ace-editor";
|
|
import BackButton from "discourse/components/back-button";
|
|
import DButton from "discourse/components/d-button";
|
|
import DTooltip from "discourse/components/d-tooltip";
|
|
import withEventValue from "discourse/helpers/with-event-value";
|
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
import { i18n } from "discourse-i18n";
|
|
import ComboBox from "select-kit/components/combo-box";
|
|
import AiToolParameterEditor from "./ai-tool-parameter-editor";
|
|
import AiToolTestModal from "./modal/ai-tool-test-modal";
|
|
import RagOptions from "./rag-options";
|
|
import RagUploader from "./rag-uploader";
|
|
|
|
const ACE_EDITOR_MODE = "javascript";
|
|
const ACE_EDITOR_THEME = "chrome";
|
|
|
|
export default class AiToolEditor extends Component {
|
|
@service router;
|
|
@service dialog;
|
|
@service modal;
|
|
@service toasts;
|
|
@service store;
|
|
@service siteSettings;
|
|
|
|
@tracked isSaving = false;
|
|
@tracked editingModel = null;
|
|
@tracked showDelete = false;
|
|
@tracked selectedPreset = null;
|
|
|
|
get presets() {
|
|
return this.args.presets.map((preset) => {
|
|
return {
|
|
name: preset.preset_name,
|
|
id: preset.preset_id,
|
|
};
|
|
});
|
|
}
|
|
|
|
get showPresets() {
|
|
return !this.selectedPreset && this.args.model.isNew;
|
|
}
|
|
|
|
@action
|
|
updateModel() {
|
|
this.editingModel = this.args.model.workingCopy();
|
|
this.showDelete = !this.args.model.isNew;
|
|
}
|
|
|
|
@action
|
|
configurePreset() {
|
|
this.selectedPreset = this.args.presets.findBy("preset_id", this.presetId);
|
|
this.editingModel = this.store
|
|
.createRecord("ai-tool", this.selectedPreset)
|
|
.workingCopy();
|
|
this.showDelete = false;
|
|
}
|
|
|
|
@action
|
|
updateUploads(uploads) {
|
|
this.editingModel.rag_uploads = uploads;
|
|
}
|
|
|
|
@action
|
|
removeUpload(upload) {
|
|
this.editingModel.rag_uploads.removeObject(upload);
|
|
if (!this.args.model.isNew) {
|
|
this.save();
|
|
}
|
|
}
|
|
|
|
@action
|
|
async save() {
|
|
this.isSaving = true;
|
|
|
|
try {
|
|
const data = this.editingModel.getProperties(
|
|
"name",
|
|
"description",
|
|
"parameters",
|
|
"script",
|
|
"summary",
|
|
"rag_uploads",
|
|
"rag_chunk_tokens",
|
|
"rag_chunk_overlap_tokens"
|
|
);
|
|
|
|
await this.args.model.save(data);
|
|
|
|
this.toasts.success({
|
|
data: { message: i18n("discourse_ai.tools.saved") },
|
|
duration: 2000,
|
|
});
|
|
if (!this.args.tools.any((tool) => tool.id === this.args.model.id)) {
|
|
this.args.tools.pushObject(this.args.model);
|
|
}
|
|
|
|
this.router.transitionTo(
|
|
"adminPlugins.show.discourse-ai-tools.edit",
|
|
this.args.model
|
|
);
|
|
} catch (e) {
|
|
popupAjaxError(e);
|
|
} finally {
|
|
this.isSaving = false;
|
|
}
|
|
}
|
|
|
|
@action
|
|
delete() {
|
|
return this.dialog.confirm({
|
|
message: i18n("discourse_ai.tools.confirm_delete"),
|
|
didConfirm: async () => {
|
|
await this.args.model.destroyRecord();
|
|
|
|
this.args.tools.removeObject(this.args.model);
|
|
this.router.transitionTo("adminPlugins.show.discourse-ai-tools.index");
|
|
},
|
|
});
|
|
}
|
|
|
|
@action
|
|
openTestModal() {
|
|
this.modal.show(AiToolTestModal, {
|
|
model: {
|
|
tool: this.editingModel,
|
|
},
|
|
});
|
|
}
|
|
|
|
<template>
|
|
<BackButton
|
|
@route="adminPlugins.show.discourse-ai-tools"
|
|
@label="discourse_ai.tools.back"
|
|
/>
|
|
|
|
<form
|
|
{{didInsert this.updateModel @model.id}}
|
|
{{didUpdate this.updateModel @model.id}}
|
|
class="form-horizontal ai-tool-editor"
|
|
>
|
|
{{#if this.showPresets}}
|
|
<div class="control-group">
|
|
<label>{{i18n "discourse_ai.tools.presets"}}</label>
|
|
<ComboBox
|
|
@value={{this.presetId}}
|
|
@content={{this.presets}}
|
|
class="ai-tool-editor__presets"
|
|
/>
|
|
</div>
|
|
|
|
<div class="control-group ai-llm-editor__action_panel">
|
|
<DButton
|
|
@action={{this.configurePreset}}
|
|
@label="discourse_ai.tools.next.title"
|
|
class="ai-tool-editor__next"
|
|
/>
|
|
</div>
|
|
{{else}}
|
|
<div class="control-group">
|
|
<label>{{i18n "discourse_ai.tools.name"}}</label>
|
|
<input
|
|
{{on "input" (withEventValue (fn (mut this.editingModel.name)))}}
|
|
value={{this.editingModel.name}}
|
|
type="text"
|
|
class="ai-tool-editor__name"
|
|
/>
|
|
<DTooltip
|
|
@icon="circle-question"
|
|
@content={{i18n "discourse_ai.tools.name_help"}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>{{i18n "discourse_ai.tools.description"}}</label>
|
|
<textarea
|
|
{{on
|
|
"input"
|
|
(withEventValue (fn (mut this.editingModel.description)))
|
|
}}
|
|
placeholder={{i18n "discourse_ai.tools.description_help"}}
|
|
class="ai-tool-editor__description input-xxlarge"
|
|
>{{this.editingModel.description}}</textarea>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>{{i18n "discourse_ai.tools.summary"}}</label>
|
|
<input
|
|
{{on "input" (withEventValue (fn (mut this.editingModel.summary)))}}
|
|
value={{this.editingModel.summary}}
|
|
type="text"
|
|
class="ai-tool-editor__summary input-xxlarge"
|
|
/>
|
|
<DTooltip
|
|
@icon="circle-question"
|
|
@content={{i18n "discourse_ai.tools.summary_help"}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>{{i18n "discourse_ai.tools.parameters"}}</label>
|
|
<AiToolParameterEditor @parameters={{this.editingModel.parameters}} />
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>{{i18n "discourse_ai.tools.script"}}</label>
|
|
<AceEditor
|
|
@content={{this.editingModel.script}}
|
|
@onChange={{fn (mut this.editingModel.script)}}
|
|
@mode={{ACE_EDITOR_MODE}}
|
|
@theme={{ACE_EDITOR_THEME}}
|
|
@editorId="ai-tool-script-editor"
|
|
/>
|
|
</div>
|
|
|
|
{{#if this.siteSettings.ai_embeddings_enabled}}
|
|
<div class="control-group">
|
|
<RagUploader
|
|
@target={{this.editingModel}}
|
|
@updateUploads={{this.updateUploads}}
|
|
@onRemove={{this.removeUpload}}
|
|
/>
|
|
</div>
|
|
<RagOptions @model={{this.editingModel}} />
|
|
{{/if}}
|
|
|
|
<div class="control-group ai-tool-editor__action_panel">
|
|
{{#unless @model.isNew}}
|
|
<DButton
|
|
@action={{this.openTestModal}}
|
|
@label="discourse_ai.tools.test"
|
|
class="ai-tool-editor__test-button"
|
|
/>
|
|
{{/unless}}
|
|
|
|
<DButton
|
|
@action={{this.save}}
|
|
@label="discourse_ai.tools.save"
|
|
@disabled={{this.isSaving}}
|
|
class="btn-primary ai-tool-editor__save"
|
|
/>
|
|
|
|
{{#if this.showDelete}}
|
|
<DButton
|
|
@action={{this.delete}}
|
|
@label="discourse_ai.tools.delete"
|
|
class="btn-danger ai-tool-editor__delete"
|
|
/>
|
|
{{/if}}
|
|
</div>
|
|
{{/if}}
|
|
</form>
|
|
</template>
|
|
}
|