@@ -47,6 +47,7 @@ def create
4747
4848 if llm_model . save
4949 llm_model . toggle_companion_user
50+ log_llm_model_creation ( llm_model )
5051 render json : LlmModelSerializer . new ( llm_model ) , status : :created
5152 else
5253 render_json_error llm_model
@@ -56,6 +57,10 @@ def create
5657 def update
5758 llm_model = LlmModel . find ( params [ :id ] )
5859
60+ # Capture initial state for logging
61+ initial_attributes = llm_model . attributes . dup
62+ initial_quotas = llm_model . llm_quotas . map ( &:attributes )
63+
5964 if params [ :ai_llm ] . key? ( :llm_quotas )
6065 if quota_params
6166 existing_quota_group_ids = llm_model . llm_quotas . pluck ( :group_id )
@@ -81,6 +86,7 @@ def update
8186
8287 if llm_model . update ( ai_llm_params ( updating : llm_model ) )
8388 llm_model . toggle_companion_user
89+ log_llm_model_update ( llm_model , initial_attributes , initial_quotas )
8490 render json : LlmModelSerializer . new ( llm_model )
8591 else
8692 render_json_error llm_model
@@ -109,11 +115,20 @@ def destroy
109115 )
110116 end
111117
118+ # Capture model details for logging before destruction
119+ model_details = {
120+ model_id : llm_model . id ,
121+ display_name : llm_model . display_name ,
122+ name : llm_model . name ,
123+ provider : llm_model . provider
124+ }
125+
112126 # Clean up companion users
113127 llm_model . enabled_chat_bot = false
114128 llm_model . toggle_companion_user
115129
116130 if llm_model . destroy
131+ log_llm_model_deletion ( model_details )
117132 head :no_content
118133 else
119134 render_json_error llm_model
@@ -190,6 +205,96 @@ def ai_llm_params(updating: nil)
190205
191206 permitted
192207 end
208+
209+ def log_llm_model_creation ( llm_model )
210+ log_details = {
211+ model_id : llm_model . id ,
212+ display_name : llm_model . display_name ,
213+ name : llm_model . name ,
214+ provider : llm_model . provider ,
215+ tokenizer : llm_model . tokenizer ,
216+ enabled_chat_bot : llm_model . enabled_chat_bot ,
217+ vision_enabled : llm_model . vision_enabled ,
218+ }
219+
220+ # Add cost details if present
221+ if llm_model . input_cost . present?
222+ log_details [ :input_cost ] = llm_model . input_cost
223+ end
224+ if llm_model . output_cost . present?
225+ log_details [ :output_cost ] = llm_model . output_cost
226+ end
227+ if llm_model . cached_input_cost . present?
228+ log_details [ :cached_input_cost ] = llm_model . cached_input_cost
229+ end
230+
231+ # Add quota information if present
232+ if llm_model . llm_quotas . any?
233+ log_details [ :quotas ] = llm_model . llm_quotas . map do |quota |
234+ "Group #{ quota . group_id } : #{ quota . max_tokens } tokens, #{ quota . max_usages } usages, #{ quota . duration_seconds } s"
235+ end . join ( "; " )
236+ end
237+
238+ StaffActionLogger . new ( current_user ) . log_custom (
239+ "create_ai_llm_model" ,
240+ log_details
241+ )
242+ end
243+
244+ def log_llm_model_update ( llm_model , initial_attributes , initial_quotas )
245+ current_attributes = llm_model . attributes
246+ current_quotas = llm_model . llm_quotas . reload . map ( &:attributes )
247+
248+ # Track changes to main attributes
249+ changes = { }
250+ trackable_fields = %w[
251+ display_name name provider tokenizer url max_prompt_tokens max_output_tokens
252+ enabled_chat_bot vision_enabled input_cost output_cost cached_input_cost
253+ provider_params
254+ ]
255+
256+ trackable_fields . each do |field |
257+ initial_value = initial_attributes [ field ]
258+ current_value = current_attributes [ field ]
259+
260+ if initial_value != current_value
261+ # Handle API key specially - don't log the actual values for security
262+ if field == "api_key"
263+ changes [ field ] = initial_value . present? && current_value . present? ?
264+ "updated" : ( current_value . present? ? "set" : "removed" )
265+ else
266+ changes [ field ] = "#{ initial_value } → #{ current_value } "
267+ end
268+ end
269+ end
270+
271+ # Track quota changes
272+ if initial_quotas != current_quotas
273+ initial_quota_summary = initial_quotas . map { |q | "Group #{ q [ 'group_id' ] } : #{ q [ 'max_tokens' ] } tokens" } . join ( "; " )
274+ current_quota_summary = current_quotas . map { |q | "Group #{ q [ 'group_id' ] } : #{ q [ 'max_tokens' ] } tokens" } . join ( "; " )
275+ changes [ :quotas ] = "#{ initial_quota_summary } → #{ current_quota_summary } "
276+ end
277+
278+ # Only log if there are actual changes
279+ if changes . any?
280+ log_details = {
281+ model_id : llm_model . id ,
282+ model_name : llm_model . display_name || llm_model . name ,
283+ } . merge ( changes )
284+
285+ StaffActionLogger . new ( current_user ) . log_custom (
286+ "update_ai_llm_model" ,
287+ log_details
288+ )
289+ end
290+ end
291+
292+ def log_llm_model_deletion ( model_details )
293+ StaffActionLogger . new ( current_user ) . log_custom (
294+ "delete_ai_llm_model" ,
295+ model_details
296+ )
297+ end
193298 end
194299 end
195300end
0 commit comments