From 1742f42257e8977cf66278de8d2d664c7a44adc2 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Wed, 16 Apr 2025 13:41:19 -0700 Subject: [PATCH 1/9] DEV: Cannot access seeded model edit page You currently cannot access the seeded model edit page, so there's no need to conditionally show certain elements based on if it's seeded. This commit removes those conditional checks. Additionally, it redirects the route if the model is seeded to avoid accessing it via the url. --- ...min-plugins-show-discourse-ai-llms-edit.js | 11 +- .../components/ai-llm-editor-form.gjs | 448 +++++++++--------- 2 files changed, 225 insertions(+), 234 deletions(-) diff --git a/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-llms-edit.js b/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-llms-edit.js index 8230211c5..f7b5de4c9 100644 --- a/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-llms-edit.js +++ b/admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-llms-edit.js @@ -2,8 +2,17 @@ import DiscourseRoute from "discourse/routes/discourse"; export default class AdminPluginsShowDiscourseAiLlmsEdit extends DiscourseRoute { async model(params) { - const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms"); const id = parseInt(params.id, 10); + + if (id < 0) { + // You shouldn't be able to access the edit page + // if the model is seeded + return this.router.transitionTo( + "adminPlugins.show.discourse-ai-llms.index" + ); + } + + const allLlms = this.modelFor("adminPlugins.show.discourse-ai-llms"); const record = allLlms.findBy("id", id); record.provider_params = record.provider_params || {}; return record; diff --git a/assets/javascripts/discourse/components/ai-llm-editor-form.gjs b/assets/javascripts/discourse/components/ai-llm-editor-form.gjs index 2eafe7cc3..fcbeccd8c 100644 --- a/assets/javascripts/discourse/components/ai-llm-editor-form.gjs +++ b/assets/javascripts/discourse/components/ai-llm-editor-form.gjs @@ -116,10 +116,6 @@ export default class AiLlmEditorForm extends Component { return localized.join(", "); } - get seeded() { - return this.args.model.id < 0; - } - get inUseWarning() { return i18n("discourse_ai.llms.in_use_warning", { settings: this.modulesUsingModel, @@ -259,15 +255,9 @@ export default class AiLlmEditorForm extends Component {
- {{#if this.seeded}} - - {{i18n "discourse_ai.llms.seeded_warning"}} - - {{/if}} - {{#if this.modulesUsingModel}} {{this.inUseWarning}} @@ -278,7 +268,6 @@ export default class AiLlmEditorForm extends Component { @name="display_name" @title={{i18n "discourse_ai.llms.display_name"}} @validation="required|length:1,100" - @disabled={{this.seeded}} @format="large" @tooltip={{i18n "discourse_ai.llms.hints.display_name"}} as |field| @@ -291,7 +280,6 @@ export default class AiLlmEditorForm extends Component { @title={{i18n "discourse_ai.llms.name"}} @tooltip={{i18n "discourse_ai.llms.hints.name"}} @validation="required" - @disabled={{this.seeded}} @format="large" as |field| > @@ -301,7 +289,6 @@ export default class AiLlmEditorForm extends Component { - {{#unless this.seeded}} - {{#if (this.canEditURL data.provider)}} - - - - {{/if}} - + {{#if (this.canEditURL data.provider)}} - + + {{/if}} - - {{#each (this.providerParamsKeys providerParamsData) as |name|}} - {{#let - (get (this.metaProviderParams data.provider) name) - as |params| - }} - - {{#if (eq params.type "enum")}} - - {{#each params.values as |option|}} - {{option.name}} - {{/each}} - - {{else if (eq params.type "checkbox")}} - - {{else}} - - {{/if}} - - {{/let}} - {{/each}} - + + + - - - {{#each this.tokenizers as |tokenizer|}} - {{tokenizer.name}} - {{/each}} - - + + {{#each (this.providerParamsKeys providerParamsData) as |name|}} + {{#let + (get (this.metaProviderParams data.provider) name) + as |params| + }} + + {{#if (eq params.type "enum")}} + + {{#each params.values as |option|}} + {{option.name}} + {{/each}} + + {{else if (eq params.type "checkbox")}} + + {{else}} + + {{/if}} + + {{/let}} + {{/each}} + - - - + + + {{#each this.tokenizers as |tokenizer|}} + {{tokenizer.name}} + {{/each}} + + - - - + + + - - - + + + - {{#if @model.user}} - - - {{Avatar @model.user.avatar_template "small"}} - - - {{@model.user.username}} - - - {{/if}} + + + - {{#if (gt data.llm_quotas.length 0)}} - - - - - - - - - - - - - - - - - - - + + + + +
{{i18n - "discourse_ai.llms.quotas.group" - }}{{i18n - "discourse_ai.llms.quotas.max_tokens" - }}{{i18n - "discourse_ai.llms.quotas.max_usages" - }}{{i18n - "discourse_ai.llms.quotas.duration" - }}
{{collectionData.group_name}} - - - - - - - - - - - - - - - + + {{Avatar @model.user.avatar_template "small"}} + + + {{@model.user.username}} + + + {{/if}} + + {{#if (gt data.llm_quotas.length 0)}} + + + + + + + + + + + + + + + + + - - -
{{i18n + "discourse_ai.llms.quotas.group" + }}{{i18n + "discourse_ai.llms.quotas.max_tokens" + }}{{i18n + "discourse_ai.llms.quotas.max_usages" + }}{{i18n + "discourse_ai.llms.quotas.duration" + }}
{{collectionData.group_name}} + + + + + + -
-
+ +
+ + + + + + + +
+
+ + {{/if}} + + + + + + + {{#if (eq data.llm_quotas.length 0)}} {{/if}} - + {{#unless @model.isNew}} - - - - {{#if (eq data.llm_quotas.length 0)}} - - {{/if}} - - {{#unless @model.isNew}} - - {{/unless}} - - {{/unless}} + {{/unless}} + {{#if this.displayTestResult}} From 1c522dbd428b52d390b3a2bd76d2231eff42f1c6 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Wed, 16 Apr 2025 15:26:40 -0700 Subject: [PATCH 2/9] DEV: add cost info to llm model --- .../discourse_ai/admin/ai_llms_controller.rb | 3 ++ app/models/llm_model.rb | 10 +++++ app/serializers/llm_model_serializer.rb | 3 ++ .../discourse/admin/models/ai-llm.js | 5 ++- .../components/ai-llm-editor-form.gjs | 39 +++++++++++++++++++ config/locales/client.en.yml | 8 ++++ ...416215039_add_cost_metrics_to_llm_model.rb | 9 +++++ 7 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20250416215039_add_cost_metrics_to_llm_model.rb diff --git a/app/controllers/discourse_ai/admin/ai_llms_controller.rb b/app/controllers/discourse_ai/admin/ai_llms_controller.rb index d9fb1e59d..cfcb12a1b 100644 --- a/app/controllers/discourse_ai/admin/ai_llms_controller.rb +++ b/app/controllers/discourse_ai/admin/ai_llms_controller.rb @@ -160,6 +160,9 @@ def ai_llm_params(updating: nil) :api_key, :enabled_chat_bot, :vision_enabled, + :input_cost, + :cached_input_cost, + :output_cost, ) provider = updating ? updating.provider : permitted[:provider] diff --git a/app/models/llm_model.rb b/app/models/llm_model.rb index 366fdef35..ac5b728bb 100644 --- a/app/models/llm_model.rb +++ b/app/models/llm_model.rb @@ -13,6 +13,13 @@ class LlmModel < ActiveRecord::Base validates :url, presence: true, unless: -> { provider == BEDROCK_PROVIDER_NAME } validates_presence_of :name, :api_key validates :max_prompt_tokens, numericality: { greater_than: 0 } + validates :input_cost, + :cached_input_cost, + :output_cost, + numericality: { + greater_than_or_equal_to: 0, + }, + allow_nil: true validate :required_provider_params scope :in_use, -> do @@ -183,4 +190,7 @@ def required_provider_params # enabled_chat_bot :boolean default(FALSE), not null # provider_params :jsonb # vision_enabled :boolean default(FALSE), not null +# input_cost :float +# cached_input_cost :float +# output_cost :float # diff --git a/app/serializers/llm_model_serializer.rb b/app/serializers/llm_model_serializer.rb index ea7d5728d..e1f7215db 100644 --- a/app/serializers/llm_model_serializer.rb +++ b/app/serializers/llm_model_serializer.rb @@ -17,6 +17,9 @@ class LlmModelSerializer < ApplicationSerializer :enabled_chat_bot, :provider_params, :vision_enabled, + :input_cost, + :output_cost, + :cached_input_cost, :used_by has_one :user, serializer: BasicUserSerializer, embed: :object diff --git a/assets/javascripts/discourse/admin/models/ai-llm.js b/assets/javascripts/discourse/admin/models/ai-llm.js index ec4bb13d8..a08fd34b5 100644 --- a/assets/javascripts/discourse/admin/models/ai-llm.js +++ b/assets/javascripts/discourse/admin/models/ai-llm.js @@ -14,7 +14,10 @@ export default class AiLlm extends RestModel { "api_key", "enabled_chat_bot", "provider_params", - "vision_enabled" + "vision_enabled", + "input_cost", + "cached_input_cost", + "output_cost" ); } diff --git a/assets/javascripts/discourse/components/ai-llm-editor-form.gjs b/assets/javascripts/discourse/components/ai-llm-editor-form.gjs index fcbeccd8c..8a892351a 100644 --- a/assets/javascripts/discourse/components/ai-llm-editor-form.gjs +++ b/assets/javascripts/discourse/components/ai-llm-editor-form.gjs @@ -61,6 +61,9 @@ export default class AiLlmEditorForm extends Component { 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 @@ -176,6 +179,7 @@ export default class AiLlmEditorForm extends Component { @action async save(data) { + console.log(data); this.isSaving = true; const isNew = this.args.model.isNew; @@ -258,6 +262,7 @@ export default class AiLlmEditorForm extends Component { class="ai-llm-editor" as |form data| > + {{log this.formData}} {{#if this.modulesUsingModel}} {{this.inUseWarning}} @@ -382,6 +387,40 @@ export default class AiLlmEditorForm extends Component { + + + + + + + + + + + + + + + + Date: Wed, 16 Apr 2025 16:49:01 -0700 Subject: [PATCH 3/9] DEV: Add total spending tile --- app/serializers/ai_usage_serializer.rb | 1 + .../discourse/components/ai-usage.gjs | 25 +++++++++++- config/locales/client.en.yml | 2 + lib/completions/report.rb | 39 +++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/app/serializers/ai_usage_serializer.rb b/app/serializers/ai_usage_serializer.rb index 38969b05b..ed0b8253f 100644 --- a/app/serializers/ai_usage_serializer.rb +++ b/app/serializers/ai_usage_serializer.rb @@ -60,6 +60,7 @@ def summary total_request_tokens: object.total_request_tokens, total_response_tokens: object.total_response_tokens, total_requests: object.total_requests, + total_spending: object.total_spending, date_range: { start: object.start_date, end: object.end_date, diff --git a/assets/javascripts/discourse/components/ai-usage.gjs b/assets/javascripts/discourse/components/ai-usage.gjs index a972726b6..b3f2caf1e 100644 --- a/assets/javascripts/discourse/components/ai-usage.gjs +++ b/assets/javascripts/discourse/components/ai-usage.gjs @@ -4,6 +4,7 @@ import { fn, hash } from "@ember/helper"; import { action } from "@ember/object"; import { LinkTo } from "@ember/routing"; import { service } from "@ember/service"; +import { modifier } from "ember-modifier"; import { eq, gt, lt } from "truth-helpers"; import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner"; import DButton from "discourse/components/d-button"; @@ -33,6 +34,22 @@ export default class AiUsage extends Component { @tracked isCustomDateActive = false; @tracked loadingData = true; + // TODO: currently doing dollar, but how should we handle other currencies? + addCurrency = modifier((element) => { + element.querySelectorAll(".d-stat-tile__label").forEach((label) => { + if ( + label.innerText.trim() === i18n("discourse_ai.usage.total_spending") + ) { + const valueElement = label + .closest(".d-stat-tile") + ?.querySelector(".d-stat-tile__value"); + if (valueElement) { + valueElement.innerText = `$${valueElement.innerText}`; + } + } + }); + }); + constructor() { super(...arguments); this.fetchData(); @@ -153,6 +170,11 @@ export default class AiUsage extends Component { value: this.data.summary.total_cached_tokens, tooltip: i18n("discourse_ai.usage.stat_tooltips.cached_tokens"), }, + { + label: i18n("discourse_ai.usage.total_spending"), + value: this.data.summary.total_spending, + tooltip: i18n("discourse_ai.usage.stat_tooltips.total_spending"), + }, ]; } @@ -376,9 +398,10 @@ export default class AiUsage extends Component { class="ai-usage__summary" > <:content> - + {{#each this.metrics as |metric|}} Date: Wed, 16 Apr 2025 17:04:56 -0700 Subject: [PATCH 4/9] DEV: Add feature/model spending breakdown --- app/serializers/ai_usage_serializer.rb | 6 +++++ .../discourse/components/ai-usage.gjs | 22 +++++++++++++++++++ lib/completions/report.rb | 22 +++++++++++++++++-- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/app/serializers/ai_usage_serializer.rb b/app/serializers/ai_usage_serializer.rb index ed0b8253f..879801ed9 100644 --- a/app/serializers/ai_usage_serializer.rb +++ b/app/serializers/ai_usage_serializer.rb @@ -22,6 +22,9 @@ def features total_cached_tokens total_request_tokens total_response_tokens + input_spending + output_spending + cached_input_spending ], ) end @@ -35,6 +38,9 @@ def models total_cached_tokens total_request_tokens total_response_tokens + input_spending + output_spending + cached_input_spending ], ) end diff --git a/assets/javascripts/discourse/components/ai-usage.gjs b/assets/javascripts/discourse/components/ai-usage.gjs index b3f2caf1e..957aa041c 100644 --- a/assets/javascripts/discourse/components/ai-usage.gjs +++ b/assets/javascripts/discourse/components/ai-usage.gjs @@ -330,6 +330,11 @@ export default class AiUsage extends Component { this.fetchData(); } + totalSpending(inputSpending, cachedSpending, outputSpending) { + const total = inputSpending + cachedSpending + outputSpending; + return `$${total.toFixed(2)}`; + } +