import Component from "@glimmer/component"; import { cached, tracked } from "@glimmer/tracking"; import { concat, fn, get } from "@ember/helper"; import { action } from "@ember/object"; import { LinkTo } from "@ember/routing"; import { later } from "@ember/runloop"; import { service } from "@ember/service"; import { eq, gt } from "truth-helpers"; import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner"; import Form from "discourse/components/form"; import Avatar from "discourse/helpers/bound-avatar-template"; import icon from "discourse/helpers/d-icon"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { i18n } from "discourse-i18n"; import AdminUser from "admin/models/admin-user"; import DurationSelector from "./ai-quota-duration-selector"; import AiLlmQuotaModal from "./modal/ai-llm-quota-modal"; export default class AiLlmEditorForm extends Component { @service toasts; @service router; @service dialog; @service modal; @tracked isSaving = false; @tracked testRunning = false; @tracked testResult = null; @tracked testError = null; @cached get formData() { if (this.args.llmTemplate) { let [id, modelName] = this.args.llmTemplate.split(/-(.*)/); if (id === "none") { return { provider_params: {} }; } const info = this.args.llms.resultSetMeta.presets.findBy("id", id); const modelInfo = info.models.findBy("name", modelName); return { max_prompt_tokens: modelInfo.tokens, max_output_tokens: modelInfo.max_output_tokens, tokenizer: info.tokenizer, url: modelInfo.endpoint || info.endpoint, display_name: modelInfo.display_name, name: modelInfo.name, provider: info.provider, provider_params: this.computeProviderParams(info.provider), input_cost: modelInfo.input_cost, output_cost: modelInfo.output_cost, cached_input_cost: modelInfo.cached_input_cost, }; } const { model } = this.args; return { max_prompt_tokens: model.max_prompt_tokens, max_output_tokens: model.max_output_tokens, api_key: model.api_key, tokenizer: model.tokenizer, url: model.url, display_name: model.display_name, name: model.name, provider: model.provider, enabled_chat_bot: model.enabled_chat_bot, vision_enabled: model.vision_enabled, input_cost: model.input_cost, output_cost: model.output_cost, cached_input_cost: model.cached_input_cost, provider_params: this.computeProviderParams( model.provider, model.provider_params ), llm_quotas: model.llm_quotas, }; } get selectedProviders() { const t = (provName) => { return i18n(`discourse_ai.llms.providers.${provName}`); }; return this.args.llms.resultSetMeta.providers .map((prov) => { return { id: prov, name: t(prov) }; }) .sort((a, b) => a.name.localeCompare(b.name)); } get tokenizers() { return this.args.llms.resultSetMeta.tokenizers.sort((a, b) => a.name.localeCompare(b.name) ); } get adminUser() { return AdminUser.create(this.args.model?.user); } get testErrorMessage() { return i18n("discourse_ai.llms.tests.failure", { error: this.testError }); } get displayTestResult() { return this.testRunning || this.testResult !== null; } get modulesUsingModel() { const usedBy = this.args.model.used_by?.filter((m) => m.type !== "ai_bot"); if (!usedBy || usedBy.length === 0) { return null; } const localized = usedBy.map((m) => { return i18n(`discourse_ai.llms.usage.${m.type}`, { persona: m.name, }); }); // TODO: this is not perfectly localized return localized.join(", "); } get inUseWarning() { return i18n("discourse_ai.llms.in_use_warning", { settings: this.modulesUsingModel, count: this.args.model.used_by.length, }); } get showAddQuotaButton() { return !this.args.model.isNew; } computeProviderParams(provider, currentParams = {}) { const params = this.args.llms.resultSetMeta.provider_params[provider] ?? {}; return Object.fromEntries( Object.entries(params).map(([k, v]) => [ k, currentParams[k] ?? (v?.type === "enum" ? v.default : null), ]) ); } @action canEditURL(provider) { return provider !== "aws_bedrock"; } @action openAddQuotaModal(addItemToCollection) { this.modal.show(AiLlmQuotaModal, { model: { llm: this.args.model, addItemToCollection }, }); } @action metaProviderParams(provider) { const params = this.args.llms.resultSetMeta.provider_params[provider] || {}; return Object.entries(params).reduce((acc, [field, value]) => { if (typeof value === "string") { acc[field] = { type: value }; } else if (typeof value === "object") { if (value.values) { value = { ...value }; value.values = value.values.map((v) => ({ id: v, name: v })); } acc[field] = { type: value.type || "text", values: value.values || [], default: value.default ?? undefined, }; } else { acc[field] = { type: "text" }; // fallback } return acc; }, {}); } @action async save(data) { this.isSaving = true; const isNew = this.args.model.isNew; const updatedData = { ...data, }; // If max_prompt_tokens input is cleared, // we want the db to store null if (!data.max_output_tokens) { updatedData.max_output_tokens = null; } try { await this.args.model.save(updatedData); if (isNew) { this.args.llms.addObject(this.args.model); this.router.transitionTo("adminPlugins.show.discourse-ai-llms.index"); } else { this.toasts.success({ data: { message: i18n("discourse_ai.llms.saved") }, duration: 2000, }); } } catch (e) { popupAjaxError(e); } finally { later(() => { this.isSaving = false; }, 1000); } } @action async test(data) { this.testRunning = true; try { const configTestResult = await this.args.model.testConfig(data); this.testResult = configTestResult.success; if (this.testResult) { this.testError = null; } else { this.testError = configTestResult.error; } } catch (e) { popupAjaxError(e); } finally { later(() => { this.testRunning = false; }, 1000); } } @action setProvider(provider, { set }) { set("provider_params", this.computeProviderParams(provider)); set("provider", provider); } @action delete() { return this.dialog.confirm({ message: i18n("discourse_ai.llms.confirm_delete"), didConfirm: () => { return this.args.model .destroyRecord() .then(() => { this.args.llms.removeObject(this.args.model); this.router.transitionTo( "adminPlugins.show.discourse-ai-llms.index" ); }) .catch(popupAjaxError); }, }); } @action providerParamsKeys(providerParams) { return providerParams ? Object.keys(providerParams) : []; } }