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 8230211c..f7b5de4c 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/app/controllers/discourse_ai/admin/ai_llms_controller.rb b/app/controllers/discourse_ai/admin/ai_llms_controller.rb index 68ddcf94..84109220 100644 --- a/app/controllers/discourse_ai/admin/ai_llms_controller.rb +++ b/app/controllers/discourse_ai/admin/ai_llms_controller.rb @@ -161,6 +161,9 @@ module DiscourseAi :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 111ac70a..05c7be4b 100644 --- a/app/models/llm_model.rb +++ b/app/models/llm_model.rb @@ -13,7 +13,14 @@ 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 :max_output_tokens, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true + validates :input_cost, + :cached_input_cost, + :output_cost, + :max_output_tokens, + numericality: { + greater_than_or_equal_to: 0, + }, + allow_nil: true validate :required_provider_params scope :in_use, -> do @@ -184,5 +191,8 @@ end # 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 # max_output_tokens :integer # diff --git a/app/serializers/ai_usage_serializer.rb b/app/serializers/ai_usage_serializer.rb index 38969b05..0a990547 100644 --- a/app/serializers/ai_usage_serializer.rb +++ b/app/serializers/ai_usage_serializer.rb @@ -22,6 +22,9 @@ class AiUsageSerializer < ApplicationSerializer total_cached_tokens total_request_tokens total_response_tokens + input_spending + output_spending + cached_input_spending ], ) end @@ -35,6 +38,9 @@ class AiUsageSerializer < ApplicationSerializer total_cached_tokens total_request_tokens total_response_tokens + input_spending + output_spending + cached_input_spending ], ) end @@ -49,6 +55,9 @@ class AiUsageSerializer < ApplicationSerializer total_cached_tokens: user.total_cached_tokens, total_request_tokens: user.total_request_tokens, total_response_tokens: user.total_response_tokens, + input_spending: user.input_spending, + output_spending: user.output_spending, + cached_input_spending: user.cached_input_spending, } end end @@ -60,6 +69,7 @@ class AiUsageSerializer < ApplicationSerializer 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/app/serializers/llm_model_serializer.rb b/app/serializers/llm_model_serializer.rb index 33e62d8b..736757d4 100644 --- a/app/serializers/llm_model_serializer.rb +++ b/app/serializers/llm_model_serializer.rb @@ -18,6 +18,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 c434ba25..cafbf3a0 100644 --- a/assets/javascripts/discourse/admin/models/ai-llm.js +++ b/assets/javascripts/discourse/admin/models/ai-llm.js @@ -15,7 +15,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 50ab2ff7..38cf1b50 100644 --- a/assets/javascripts/discourse/components/ai-llm-editor-form.gjs +++ b/assets/javascripts/discourse/components/ai-llm-editor-form.gjs @@ -47,6 +47,9 @@ export default class AiLlmEditorForm extends Component { 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, }; } @@ -63,6 +66,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 @@ -118,10 +124,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, @@ -271,15 +273,9 @@ export default class AiLlmEditorForm extends Component {
- {{#if this.seeded}} - - {{i18n "discourse_ai.llms.seeded_warning"}} - - {{/if}} - {{#if this.modulesUsingModel}} {{this.inUseWarning}} @@ -290,7 +286,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| @@ -303,7 +298,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| > @@ -313,7 +307,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}} - - - - - - - - - - - - - - - - - - - - {{#if @model.user}} - - + {{#each (this.providerParamsKeys providerParamsData) as |name|}} + {{#let + (get (this.metaProviderParams data.provider) name) + as |params| + }} + - {{Avatar @model.user.avatar_template "small"}} - - - {{@model.user.username}} - - - {{/if}} + {{#if (eq params.type "enum")}} + + {{#each params.values as |option|}} + {{option.name}} + {{/each}} + + {{else if (eq params.type "checkbox")}} + + {{else}} + + {{/if}} + + {{/let}} + {{/each}} + - {{#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}} - - - - - - - - - - - - - - - + + {{#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}} + + -
-
+ +
+ + + + + + + + + + + +
+
+ + {{/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}} diff --git a/assets/javascripts/discourse/components/ai-usage.gjs b/assets/javascripts/discourse/components/ai-usage.gjs index a972726b..0aaac6b2 100644 --- a/assets/javascripts/discourse/components/ai-usage.gjs +++ b/assets/javascripts/discourse/components/ai-usage.gjs @@ -2,6 +2,8 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { fn, hash } from "@ember/helper"; import { action } from "@ember/object"; +import didInsert from "@ember/render-modifiers/modifiers/did-insert"; +import didUpdate from "@ember/render-modifiers/modifiers/did-update"; import { LinkTo } from "@ember/routing"; import { service } from "@ember/service"; import { eq, gt, lt } from "truth-helpers"; @@ -74,6 +76,22 @@ export default class AiUsage extends Component { this.onFilterChange(); } + @action + addCurrencyChar(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}`; + } + } + }); + } + @bind takeUsers(start, end) { return this.data.users.slice(start, end); @@ -153,6 +171,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"), + }, ]; } @@ -308,6 +331,11 @@ export default class AiUsage extends Component { this.fetchData(); } + totalSpending(inputSpending, cachedSpending, outputSpending) { + const total = inputSpending + cachedSpending + outputSpending; + return `$${total.toFixed(2)}`; + } +