ruby_llm-agents 3.7.2 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/ruby_llm/agents/agents_controller.rb +14 -141
  3. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +12 -166
  4. data/app/controllers/ruby_llm/agents/executions_controller.rb +1 -1
  5. data/app/helpers/ruby_llm/agents/application_helper.rb +38 -0
  6. data/app/models/ruby_llm/agents/execution/analytics.rb +302 -103
  7. data/app/models/ruby_llm/agents/execution.rb +76 -54
  8. data/app/models/ruby_llm/agents/execution_detail.rb +2 -0
  9. data/app/models/ruby_llm/agents/tenant.rb +39 -0
  10. data/app/services/ruby_llm/agents/agent_registry.rb +98 -0
  11. data/app/views/ruby_llm/agents/executions/_list.html.erb +3 -17
  12. data/lib/generators/ruby_llm_agents/templates/add_dashboard_performance_indexes_migration.rb.tt +23 -0
  13. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +3 -0
  14. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +25 -0
  15. data/lib/ruby_llm/agents/base_agent.rb +7 -1
  16. data/lib/ruby_llm/agents/core/configuration.rb +1 -0
  17. data/lib/ruby_llm/agents/core/instrumentation.rb +15 -19
  18. data/lib/ruby_llm/agents/core/version.rb +1 -1
  19. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +4 -4
  20. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +19 -11
  21. data/lib/ruby_llm/agents/pipeline/builder.rb +8 -4
  22. data/lib/ruby_llm/agents/pipeline/context.rb +43 -1
  23. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +6 -4
  24. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +6 -4
  25. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +26 -75
  26. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +6 -6
  27. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +23 -27
  28. data/lib/ruby_llm/agents/providers/inception/capabilities.rb +107 -0
  29. data/lib/ruby_llm/agents/providers/inception/chat.rb +17 -0
  30. data/lib/ruby_llm/agents/providers/inception/configuration.rb +9 -0
  31. data/lib/ruby_llm/agents/providers/inception/models.rb +38 -0
  32. data/lib/ruby_llm/agents/providers/inception/registry.rb +45 -0
  33. data/lib/ruby_llm/agents/providers/inception.rb +50 -0
  34. data/lib/ruby_llm/agents/results/base.rb +4 -2
  35. data/lib/ruby_llm/agents/results/image_analysis_result.rb +4 -2
  36. data/lib/ruby_llm/agents/text/embedder.rb +4 -0
  37. data/lib/ruby_llm/agents.rb +4 -0
  38. metadata +8 -1
@@ -170,7 +170,8 @@ module RubyLLM
170
170
  # @return [Array<Hash>] Middleware entries
171
171
  def global_middleware_entries
172
172
  RubyLLM::Agents.configuration.middleware_stack
173
- rescue
173
+ rescue => e
174
+ Rails.logger.debug("[RubyLLM::Agents::Pipeline] Failed to load global middleware: #{e.message}") if defined?(Rails) && Rails.logger
174
175
  []
175
176
  end
176
177
 
@@ -182,7 +183,8 @@ module RubyLLM
182
183
  return [] unless agent_class&.respond_to?(:agent_middleware)
183
184
 
184
185
  agent_class.agent_middleware
185
- rescue
186
+ rescue => e
187
+ Rails.logger.debug("[RubyLLM::Agents::Pipeline] Failed to load agent middleware: #{e.message}") if defined?(Rails) && Rails.logger
186
188
  []
187
189
  end
188
190
 
@@ -207,7 +209,8 @@ module RubyLLM
207
209
  # @return [Boolean]
208
210
  def budgets_enabled?
209
211
  RubyLLM::Agents.configuration.budgets_enabled?
210
- rescue
212
+ rescue => e
213
+ Rails.logger.debug("[RubyLLM::Agents::Pipeline] Failed to check budgets_enabled: #{e.message}") if defined?(Rails) && Rails.logger
211
214
  false
212
215
  end
213
216
 
@@ -248,7 +251,8 @@ module RubyLLM
248
251
 
249
252
  (retries.is_a?(Integer) && retries.positive?) ||
250
253
  (fallbacks.is_a?(Array) && fallbacks.any?)
251
- rescue
254
+ rescue => e
255
+ Rails.logger.debug("[RubyLLM::Agents::Pipeline] Failed to check reliability_enabled: #{e.message}") if defined?(Rails) && Rails.logger
252
256
  false
253
257
  end
254
258
  end
@@ -136,6 +136,24 @@ module RubyLLM
136
136
  (@input_tokens || 0) + (@output_tokens || 0)
137
137
  end
138
138
 
139
+ # Returns a RubyLLM interface scoped to tenant API keys when present.
140
+ #
141
+ # When tenant API keys are stored on this context (by the Tenant middleware),
142
+ # returns a RubyLLM::Context with a cloned config that has tenant-specific
143
+ # keys applied. This avoids mutating global RubyLLM configuration, making
144
+ # multi-tenant LLM calls thread-safe.
145
+ #
146
+ # When no tenant API keys are present, returns the RubyLLM module directly
147
+ # (which uses the global configuration).
148
+ #
149
+ # @return [RubyLLM::Context, RubyLLM] Scoped context or global module
150
+ def llm
151
+ api_keys = self[:tenant_api_keys]
152
+ return RubyLLM if api_keys.nil? || api_keys.empty?
153
+
154
+ @llm_context ||= build_llm_context(api_keys)
155
+ end
156
+
139
157
  # Custom metadata storage - read
140
158
  #
141
159
  # @param key [Symbol, String] The metadata key
@@ -217,6 +235,24 @@ module RubyLLM
217
235
 
218
236
  private
219
237
 
238
+ # Builds a RubyLLM::Context with tenant-specific API keys
239
+ #
240
+ # Clones the global RubyLLM config and overlays tenant API keys,
241
+ # then wraps it in a RubyLLM::Context for thread-safe per-request use.
242
+ #
243
+ # @param api_keys [Hash] Provider => key mappings (e.g., {openai: "sk-..."})
244
+ # @return [RubyLLM::Context] Context with tenant-scoped configuration
245
+ def build_llm_context(api_keys)
246
+ config = RubyLLM.config.dup
247
+ api_keys.each do |provider, key|
248
+ next if key.nil? || (key.respond_to?(:empty?) && key.empty?)
249
+
250
+ setter = "#{provider}_api_key="
251
+ config.public_send(setter, key) if config.respond_to?(setter)
252
+ end
253
+ RubyLLM::Context.new(config)
254
+ end
255
+
220
256
  # Extracts agent_type from the agent class
221
257
  #
222
258
  # @param agent_class [Class] The agent class
@@ -227,7 +263,13 @@ module RubyLLM
227
263
  if agent_class.respond_to?(:agent_type)
228
264
  agent_class.agent_type
229
265
  else
230
- # Infer from class name as fallback
266
+ if defined?(RubyLLM::Agents::Deprecations)
267
+ RubyLLM::Agents::Deprecations.warn(
268
+ "#{agent_class.name || agent_class} does not define `agent_type`. " \
269
+ "Guessing from class name. Define `self.agent_type` to silence this warning.",
270
+ caller
271
+ )
272
+ end
231
273
  infer_agent_type(agent_class)
232
274
  end
233
275
  end
@@ -64,8 +64,8 @@ module RubyLLM
64
64
  tenant_id: context.tenant_id
65
65
  }.merge(extras)
66
66
  )
67
- rescue
68
- # Never let notifications break execution
67
+ rescue => e
68
+ debug("Budget notification failed: #{e.message}")
69
69
  end
70
70
 
71
71
  # Returns whether budgets are enabled globally
@@ -73,7 +73,8 @@ module RubyLLM
73
73
  # @return [Boolean]
74
74
  def budgets_enabled?
75
75
  global_config.budgets_enabled?
76
- rescue
76
+ rescue => e
77
+ debug("Failed to check budgets_enabled config: #{e.message}")
77
78
  false
78
79
  end
79
80
 
@@ -104,7 +105,8 @@ module RubyLLM
104
105
  emit_budget_notification("ruby_llm_agents.budget.exceeded", context)
105
106
  raise
106
107
  rescue => e
107
- error("Budget check failed: #{e.message}")
108
+ # Log at error level so unexpected failures are visible in logs
109
+ error("Budget check failed: #{e.class}: #{e.message}")
108
110
  end
109
111
 
110
112
  # Records spend after execution
@@ -73,8 +73,8 @@ module RubyLLM
73
73
  agent_type: @agent_class&.name,
74
74
  cache_key: cache_key
75
75
  )
76
- rescue
77
- # Never let notifications break execution
76
+ rescue => e
77
+ debug("Cache notification failed: #{e.message}")
78
78
  end
79
79
 
80
80
  # Returns whether caching is enabled for this agent
@@ -89,7 +89,8 @@ module RubyLLM
89
89
  # @return [ActiveSupport::Cache::Store, nil]
90
90
  def cache_store
91
91
  global_config.cache_store
92
- rescue
92
+ rescue => e
93
+ debug("Failed to access cache_store config: #{e.message}")
93
94
  nil
94
95
  end
95
96
 
@@ -148,7 +149,8 @@ module RubyLLM
148
149
  else
149
150
  input.to_json
150
151
  end
151
- rescue
152
+ rescue => e
153
+ debug("Failed to serialize input for cache key: #{e.message}")
152
154
  input.to_s
153
155
  end
154
156
 
@@ -176,8 +176,8 @@ module RubyLLM
176
176
  else
177
177
  RubyLLM::Agents::ExecutionDetail.create!(detail_attrs.merge(execution_id: execution.id))
178
178
  end
179
- rescue
180
- # Non-critical
179
+ rescue => e
180
+ debug("Failed to store error detail: #{e.message}")
181
181
  end
182
182
  rescue => e
183
183
  error("CRITICAL: Failed emergency status update for execution #{execution&.id}: #{e.message}")
@@ -205,8 +205,8 @@ module RubyLLM
205
205
  tenant_id: context.tenant_id,
206
206
  execution_id: context.execution_id
207
207
  )
208
- rescue
209
- # Never let notifications break execution
208
+ rescue => e
209
+ debug("Start notification failed: #{e.message}")
210
210
  end
211
211
 
212
212
  # Emits an AS::Notification for execution completion or error
@@ -242,8 +242,8 @@ module RubyLLM
242
242
  error_class: context.error&.class&.name,
243
243
  error_message: context.error&.message
244
244
  )
245
- rescue
246
- # Never let notifications break execution
245
+ rescue => e
246
+ debug("Complete notification failed: #{e.message}")
247
247
  end
248
248
 
249
249
  # Builds data for initial running execution record
@@ -321,7 +321,8 @@ module RubyLLM
321
321
 
322
322
  context_meta = begin
323
323
  context.metadata.dup
324
- rescue
324
+ rescue => e
325
+ debug("Failed to read context metadata: #{e.message}")
325
326
  {}
326
327
  end
327
328
  context_meta.transform_keys!(&:to_s)
@@ -414,72 +415,19 @@ module RubyLLM
414
415
  error("Failed to record execution: #{e.message}")
415
416
  end
416
417
 
417
- # Builds execution data hash
418
+ # Builds execution data hash for the legacy single-step persistence path.
419
+ #
420
+ # Composes from build_running_execution_data and build_completion_data
421
+ # to avoid duplication.
418
422
  #
419
423
  # @param context [Context] The execution context
420
424
  # @param status [String] "success" or "error"
421
- # @return [Hash] Execution data
425
+ # @return [Hash] Execution data with _detail_data for detail record
422
426
  def build_execution_data(context, status)
423
- # Merge metadata: agent metadata (base) < middleware metadata (overlay)
424
- agent_meta = safe_agent_metadata(context)
425
- merged_metadata = agent_meta.transform_keys(&:to_s)
426
-
427
- context_meta = begin
428
- context.metadata.dup
429
- rescue
430
- {}
431
- end
432
- context_meta.transform_keys!(&:to_s)
433
- merged_metadata.merge!(context_meta)
434
-
435
- if context.cached? && context[:cache_key]
436
- merged_metadata["response_cache_key"] = context[:cache_key]
437
- end
438
-
439
- data = {
440
- agent_type: context.agent_class&.name,
441
- model_id: context.model,
442
- status: determine_status(context, status),
443
- duration_ms: context.duration_ms,
444
- started_at: context.started_at,
445
- completed_at: context.completed_at,
446
- cache_hit: context.cached?,
447
- input_tokens: context.input_tokens || 0,
448
- output_tokens: context.output_tokens || 0,
449
- total_cost: context.total_cost || 0,
450
- attempts_count: context.attempts_made,
451
- metadata: merged_metadata
452
- }
453
-
454
- # Extract tracing fields from agent metadata to dedicated columns
455
- if agent_meta.any?
456
- data[:trace_id] = agent_meta[:trace_id] if agent_meta[:trace_id]
457
- data[:request_id] = agent_meta[:request_id] if agent_meta[:request_id]
458
- data[:parent_execution_id] = agent_meta[:parent_execution_id] if agent_meta[:parent_execution_id]
459
- data[:root_execution_id] = agent_meta[:root_execution_id] if agent_meta[:root_execution_id]
460
- end
461
-
462
- # Add tenant_id only if multi-tenancy is enabled and tenant is set
463
- if global_config.multi_tenancy_enabled? && context.tenant_id.present?
464
- data[:tenant_id] = context.tenant_id
465
- end
466
-
467
- # Error class on execution
468
- if context.error
469
- data[:error_class] = context.error.class.name
470
- end
471
-
472
- # Tool calls count on execution
473
- if context[:tool_calls].present?
474
- data[:tool_calls_count] = context[:tool_calls].size
475
- end
476
-
477
- # Attempts count on execution
478
- if context[:reliability_attempts].present?
479
- data[:attempts_count] = context[:reliability_attempts].size
480
- end
427
+ data = build_running_execution_data(context)
428
+ .merge(build_completion_data(context, determine_status(context, status)))
481
429
 
482
- # Store detail data for separate creation
430
+ # Build detail data for separate creation
483
431
  detail_data = {parameters: sanitize_parameters(context)}
484
432
  if global_config.persist_prompts
485
433
  exec_opts = context.options[:options] || {}
@@ -494,11 +442,9 @@ module RubyLLM
494
442
  detail_data[:response] = serialize_response(context)
495
443
  end
496
444
 
497
- # Persist audio data for Speaker executions
498
445
  maybe_persist_audio_response(context, detail_data)
499
446
 
500
447
  data[:_detail_data] = detail_data
501
-
502
448
  data
503
449
  end
504
450
 
@@ -527,7 +473,8 @@ module RubyLLM
527
473
 
528
474
  params = begin
529
475
  context.agent_instance.send(:options)
530
- rescue
476
+ rescue => e
477
+ debug("Failed to extract agent options: #{e.message}")
531
478
  {}
532
479
  end
533
480
  params = params.dup
@@ -689,7 +636,8 @@ module RubyLLM
689
636
  else
690
637
  cfg.track_executions
691
638
  end
692
- rescue
639
+ rescue => e
640
+ debug("Failed to check tracking config: #{e.message}")
693
641
  false
694
642
  end
695
643
 
@@ -698,7 +646,8 @@ module RubyLLM
698
646
  # @return [Boolean]
699
647
  def track_cache_hits?
700
648
  global_config.respond_to?(:track_cache_hits) && global_config.track_cache_hits
701
- rescue
649
+ rescue => e
650
+ debug("Failed to check track_cache_hits config: #{e.message}")
702
651
  false
703
652
  end
704
653
 
@@ -707,7 +656,8 @@ module RubyLLM
707
656
  # @return [Boolean]
708
657
  def async_logging?
709
658
  global_config.async_logging && defined?(Infrastructure::ExecutionLoggerJob)
710
- rescue
659
+ rescue => e
660
+ debug("Failed to check async_logging config: #{e.message}")
711
661
  false
712
662
  end
713
663
 
@@ -722,7 +672,8 @@ module RubyLLM
722
672
  @_assistant_prompt_column_exists = begin
723
673
  defined?(RubyLLM::Agents::ExecutionDetail) &&
724
674
  RubyLLM::Agents::ExecutionDetail.column_names.include?("assistant_prompt")
725
- rescue
675
+ rescue => e
676
+ debug("Failed to check assistant_prompt column: #{e.message}")
726
677
  false
727
678
  end
728
679
  end
@@ -237,7 +237,7 @@ module RubyLLM
237
237
  return false if attempt_index >= max_retries
238
238
  return false if total_deadline && Time.current > total_deadline
239
239
  # Don't retry if fallback models are available — move to next model instead
240
- return false if has_fallback_models?(config)
240
+ return false if fallback_models?(config)
241
241
 
242
242
  retryable_error?(error, config)
243
243
  end
@@ -256,7 +256,7 @@ module RubyLLM
256
256
  #
257
257
  # @param config [Hash] The reliability configuration
258
258
  # @return [Boolean]
259
- def has_fallback_models?(config)
259
+ def fallback_models?(config)
260
260
  fallbacks = config[:fallback_models]
261
261
  fallbacks.is_a?(Array) && fallbacks.any?
262
262
  end
@@ -317,8 +317,8 @@ module RubyLLM
317
317
  event,
318
318
  {agent_type: @agent_class&.name}.merge(extras)
319
319
  )
320
- rescue
321
- # Never let notifications break execution
320
+ rescue => e
321
+ debug("Reliability notification failed: #{e.message}")
322
322
  end
323
323
 
324
324
  # Sleeps without blocking other fibers when in async context
@@ -333,8 +333,8 @@ module RubyLLM
333
333
  else
334
334
  sleep(seconds)
335
335
  end
336
- rescue
337
- # Fall back to regular sleep if async detection fails
336
+ rescue => e
337
+ debug("Async sleep failed, falling back to regular sleep: #{e.message}")
338
338
  sleep(seconds)
339
339
  end
340
340
  end
@@ -112,7 +112,7 @@ module RubyLLM
112
112
  ensure_tenant_for_model!(tenant_object)
113
113
  else
114
114
  # For hash-based or string tenants, ensure a minimal record exists
115
- RubyLLM::Agents::Tenant.find_or_create_by!(tenant_id: context.tenant_id)
115
+ find_or_create_tenant!(context.tenant_id)
116
116
  end
117
117
  rescue => e
118
118
  # Don't fail the execution if tenant record creation fails
@@ -145,6 +145,18 @@ module RubyLLM
145
145
  enforcement: options[:enforcement]&.to_s || "soft",
146
146
  inherit_global_defaults: options.fetch(:inherit_global, true)
147
147
  )
148
+ rescue ActiveRecord::RecordNotUnique
149
+ # Race condition: another thread created the record — safe to ignore
150
+ end
151
+
152
+ # Finds or creates a tenant record, handling race conditions
153
+ #
154
+ # @param tenant_id [String] The tenant identifier
155
+ def find_or_create_tenant!(tenant_id)
156
+ RubyLLM::Agents::Tenant.find_or_create_by!(tenant_id: tenant_id)
157
+ rescue ActiveRecord::RecordNotUnique
158
+ # Another thread/process created the record — just find it
159
+ RubyLLM::Agents::Tenant.find_by!(tenant_id: tenant_id)
148
160
  end
149
161
 
150
162
  # Checks if the tenants table exists (memoized)
@@ -154,7 +166,8 @@ module RubyLLM
154
166
  return @tenant_table_exists if defined?(@tenant_table_exists)
155
167
 
156
168
  @tenant_table_exists = ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenants)
157
- rescue
169
+ rescue => e
170
+ debug("Failed to check tenant table existence: #{e.message}")
158
171
  @tenant_table_exists = false
159
172
  end
160
173
 
@@ -178,7 +191,11 @@ module RubyLLM
178
191
  apply_tenant_object_api_keys!(context)
179
192
  end
180
193
 
181
- # Applies API keys from tenant object's llm_api_keys method
194
+ # Stores tenant API keys on the context for thread-safe per-request use.
195
+ #
196
+ # Instead of mutating the global RubyLLM configuration (which is not
197
+ # thread-safe), keys are stored on the context. The Pipeline::Context#llm
198
+ # method creates a scoped RubyLLM::Context with these keys when needed.
182
199
  #
183
200
  # @param context [Context] The execution context
184
201
  def apply_tenant_object_api_keys!(context)
@@ -188,34 +205,12 @@ module RubyLLM
188
205
  api_keys = tenant_object.llm_api_keys
189
206
  return if api_keys.blank?
190
207
 
191
- apply_api_keys_to_ruby_llm(api_keys)
208
+ context[:tenant_api_keys] = api_keys
192
209
  rescue => e
193
210
  # Log but don't fail if API key extraction fails
194
211
  warn_api_key_error("tenant object", e)
195
212
  end
196
213
 
197
- # Applies a hash of API keys to RubyLLM configuration
198
- #
199
- # @param api_keys [Hash] Hash of provider => key mappings
200
- def apply_api_keys_to_ruby_llm(api_keys)
201
- RubyLLM.configure do |config|
202
- api_keys.each do |provider, key|
203
- next if key.blank?
204
-
205
- setter = api_key_setter_for(provider)
206
- config.public_send(setter, key) if config.respond_to?(setter)
207
- end
208
- end
209
- end
210
-
211
- # Returns the setter method name for a provider's API key
212
- #
213
- # @param provider [Symbol, String] Provider name (e.g., :openai, :anthropic)
214
- # @return [String] Setter method name (e.g., "openai_api_key=")
215
- def api_key_setter_for(provider)
216
- "#{provider}_api_key="
217
- end
218
-
219
214
  # Logs a warning about API key resolution failure
220
215
  #
221
216
  # @param source [String] Source that failed
@@ -236,7 +231,8 @@ module RubyLLM
236
231
  return nil unless tenant.respond_to?(:llm_config)
237
232
 
238
233
  tenant.llm_config
239
- rescue
234
+ rescue => e
235
+ debug("Failed to extract tenant config: #{e.message}")
240
236
  nil
241
237
  end
242
238
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ module Providers
6
+ class Inception
7
+ # Determines capabilities and pricing for Inception Mercury models.
8
+ #
9
+ # Mercury models are diffusion LLMs with text-only I/O.
10
+ # Pricing is per million tokens.
11
+ #
12
+ # Models:
13
+ # - mercury-2: Reasoning dLLM, function calling, structured output
14
+ # - mercury: Base chat dLLM, function calling, structured output
15
+ # - mercury-coder-small: Fast coding model
16
+ # - mercury-edit: Code editing/FIM model
17
+ module Capabilities
18
+ module_function
19
+
20
+ REASONING_MODELS = %w[mercury-2].freeze
21
+ CODER_MODELS = %w[mercury-coder-small mercury-edit].freeze
22
+ FUNCTION_CALLING_MODELS = %w[mercury-2 mercury].freeze
23
+
24
+ def context_window_for(_model_id)
25
+ 128_000
26
+ end
27
+
28
+ def max_tokens_for(_model_id)
29
+ 32_000
30
+ end
31
+
32
+ def input_price_for(_model_id)
33
+ 0.25
34
+ end
35
+
36
+ def output_price_for(model_id)
37
+ if CODER_MODELS.include?(model_id)
38
+ 1.00
39
+ else
40
+ 0.75
41
+ end
42
+ end
43
+
44
+ def supports_vision?(_model_id)
45
+ false
46
+ end
47
+
48
+ def supports_functions?(model_id)
49
+ FUNCTION_CALLING_MODELS.include?(model_id)
50
+ end
51
+
52
+ def supports_json_mode?(model_id)
53
+ FUNCTION_CALLING_MODELS.include?(model_id)
54
+ end
55
+
56
+ def format_display_name(model_id)
57
+ case model_id
58
+ when "mercury-2" then "Mercury 2"
59
+ when "mercury" then "Mercury"
60
+ when "mercury-coder-small" then "Mercury Coder Small"
61
+ when "mercury-edit" then "Mercury Edit"
62
+ else
63
+ model_id.split("-").map(&:capitalize).join(" ")
64
+ end
65
+ end
66
+
67
+ def model_type(model_id)
68
+ if CODER_MODELS.include?(model_id)
69
+ "code"
70
+ else
71
+ "chat"
72
+ end
73
+ end
74
+
75
+ def model_family(_model_id)
76
+ :mercury
77
+ end
78
+
79
+ def modalities_for(_model_id)
80
+ {input: ["text"], output: ["text"]}
81
+ end
82
+
83
+ def capabilities_for(model_id)
84
+ caps = ["streaming"]
85
+ if FUNCTION_CALLING_MODELS.include?(model_id)
86
+ caps << "function_calling"
87
+ caps << "structured_output"
88
+ end
89
+ caps << "reasoning" if REASONING_MODELS.include?(model_id)
90
+ caps
91
+ end
92
+
93
+ def pricing_for(model_id)
94
+ {
95
+ text_tokens: {
96
+ standard: {
97
+ input_per_million: input_price_for(model_id),
98
+ output_per_million: output_price_for(model_id)
99
+ }
100
+ }
101
+ }
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ module Providers
6
+ class Inception
7
+ # Chat methods for Inception Mercury API.
8
+ # Mercury uses standard OpenAI chat format.
9
+ module Chat
10
+ def format_role(role)
11
+ role.to_s
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extends RubyLLM::Configuration with Inception API key support.
4
+ # This allows users to configure: config.inception_api_key = ENV['INCEPTION_API_KEY']
5
+ module RubyLLM
6
+ class Configuration
7
+ attr_accessor :inception_api_key unless method_defined?(:inception_api_key)
8
+ end
9
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ module Providers
6
+ class Inception
7
+ # Parses model metadata from the Inception /models API endpoint.
8
+ # Response format is OpenAI-compatible.
9
+ module Models
10
+ module_function
11
+
12
+ def parse_list_models_response(response, slug, capabilities)
13
+ Array(response.body["data"]).map do |model_data|
14
+ model_id = model_data["id"]
15
+
16
+ ::RubyLLM::Model::Info.new(
17
+ id: model_id,
18
+ name: capabilities.format_display_name(model_id),
19
+ provider: slug,
20
+ family: "mercury",
21
+ created_at: model_data["created"] ? Time.at(model_data["created"]) : nil,
22
+ context_window: capabilities.context_window_for(model_id),
23
+ max_output_tokens: capabilities.max_tokens_for(model_id),
24
+ modalities: capabilities.modalities_for(model_id),
25
+ capabilities: capabilities.capabilities_for(model_id),
26
+ pricing: capabilities.pricing_for(model_id),
27
+ metadata: {
28
+ object: model_data["object"],
29
+ owned_by: model_data["owned_by"]
30
+ }.compact
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end