active_genie 0.30.1 → 0.30.8

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -5
  3. data/VERSION +1 -1
  4. data/lib/active_genie/comparator/debate.rb +13 -31
  5. data/lib/active_genie/comparator/fight.rb +3 -3
  6. data/lib/active_genie/comparator.rb +0 -2
  7. data/lib/active_genie/configs/base_config.rb +25 -0
  8. data/lib/active_genie/configs/extractor_config.rb +6 -14
  9. data/lib/active_genie/configs/lister_config.rb +6 -10
  10. data/lib/active_genie/configs/llm_config.rb +12 -25
  11. data/lib/active_genie/configs/log_config.rb +19 -16
  12. data/lib/active_genie/configs/providers/anthropic_config.rb +10 -16
  13. data/lib/active_genie/configs/providers/deepseek_config.rb +4 -19
  14. data/lib/active_genie/configs/providers/google_config.rb +4 -19
  15. data/lib/active_genie/configs/providers/openai_config.rb +4 -19
  16. data/lib/active_genie/configs/providers/provider_base.rb +21 -67
  17. data/lib/active_genie/configs/providers_config.rb +39 -20
  18. data/lib/active_genie/configs/ranker_config.rb +6 -12
  19. data/lib/active_genie/configuration.rb +23 -60
  20. data/lib/active_genie/{ranker/entities → entities}/player.rb +4 -0
  21. data/lib/active_genie/{ranker/entities → entities}/players.rb +1 -1
  22. data/lib/active_genie/entities/result.rb +29 -0
  23. data/lib/active_genie/errors/invalid_model_error.rb +42 -0
  24. data/lib/active_genie/errors/invalid_provider_error.rb +1 -1
  25. data/lib/active_genie/errors/provider_server_error.rb +26 -0
  26. data/lib/active_genie/errors/without_available_provider_error.rb +39 -0
  27. data/lib/active_genie/extractor/data.json +9 -0
  28. data/lib/active_genie/extractor/data.prompt.md +12 -0
  29. data/lib/active_genie/extractor/data.rb +71 -0
  30. data/lib/active_genie/extractor/explanation.rb +22 -41
  31. data/lib/active_genie/extractor/litote.rb +10 -9
  32. data/lib/active_genie/extractor.rb +5 -0
  33. data/lib/active_genie/lister/feud.json +5 -1
  34. data/lib/active_genie/lister/feud.rb +12 -17
  35. data/lib/active_genie/lister/juries.rb +18 -16
  36. data/lib/active_genie/logger.rb +16 -28
  37. data/lib/active_genie/providers/anthropic_provider.rb +12 -6
  38. data/lib/active_genie/providers/base_provider.rb +15 -18
  39. data/lib/active_genie/providers/deepseek_provider.rb +18 -10
  40. data/lib/active_genie/providers/google_provider.rb +11 -5
  41. data/lib/active_genie/providers/openai_provider.rb +8 -6
  42. data/lib/active_genie/providers/unified_provider.rb +58 -3
  43. data/lib/active_genie/ranker/elo.rb +41 -36
  44. data/lib/active_genie/ranker/free_for_all.rb +45 -28
  45. data/lib/active_genie/ranker/scoring.rb +20 -11
  46. data/lib/active_genie/ranker/tournament.rb +23 -35
  47. data/lib/active_genie/scorer/jury_bench.rb +18 -23
  48. data/lib/active_genie/utils/base_module.rb +34 -0
  49. data/lib/active_genie/utils/call_wrapper.rb +20 -0
  50. data/lib/active_genie/utils/deep_merge.rb +12 -0
  51. data/lib/active_genie/utils/fiber_by_batch.rb +2 -2
  52. data/lib/active_genie/utils/text_case.rb +18 -0
  53. data/lib/active_genie.rb +16 -18
  54. data/lib/tasks/benchmark.rake +1 -3
  55. data/lib/tasks/templates/active_genie.rb +0 -3
  56. data/lib/tasks/test.rake +62 -1
  57. metadata +25 -15
  58. data/lib/active_genie/configs/comparator_config.rb +0 -10
  59. data/lib/active_genie/configs/scorer_config.rb +0 -10
@@ -8,6 +8,10 @@
8
8
  "type": "string",
9
9
  "description": "The theme for the feud."
10
10
  },
11
+ "why_these_items": {
12
+ "type": "string",
13
+ "description": "Explanation of why these items were chosen."
14
+ },
11
15
  "items": {
12
16
  "type": "array",
13
17
  "description": "The list of items for the feud.",
@@ -16,6 +20,6 @@
16
20
  }
17
21
  }
18
22
  },
19
- "required": ["theme", "items"]
23
+ "required": ["theme", "why_these_items", "items"]
20
24
  }
21
25
  }
@@ -9,17 +9,13 @@ module ActiveGenie
9
9
  # @example Feud usage with two players and criteria
10
10
  # Feud.call("Industries that are most likely to be affected by climate change")
11
11
  #
12
- class Feud
13
- def self.call(...)
14
- new(...).call
15
- end
16
-
12
+ class Feud < ActiveGenie::BaseModule
17
13
  # @param theme [String] The theme for the feud
18
14
  # @param config [Hash] Additional configuration options
19
15
  # @return [Array of strings] List of items
20
16
  def initialize(theme, config: {})
21
17
  @theme = theme
22
- @config = ActiveGenie.configuration.merge(config)
18
+ super(config:)
23
19
  end
24
20
 
25
21
  # @return [Array of strings] The list of items
@@ -30,14 +26,17 @@ module ActiveGenie
30
26
  { role: 'user', content: "theme: #{@theme}" }
31
27
  ]
32
28
 
33
- response = ::ActiveGenie::Providers::UnifiedProvider.function_calling(
29
+ provider_response = ::ActiveGenie::Providers::UnifiedProvider.function_calling(
34
30
  messages,
35
31
  FUNCTION,
36
- config: @config
32
+ config:
37
33
  )
38
34
 
39
- log_feud(response)
40
- response['items'] || []
35
+ ActiveGenie::Result.new(
36
+ data: provider_response['items'] || [],
37
+ reasoning: provider_response['why_these_items'],
38
+ metadata: provider_response
39
+ )
41
40
  end
42
41
 
43
42
  PROMPT = File.read(File.join(__dir__, 'feud.prompt.md'))
@@ -46,15 +45,11 @@ module ActiveGenie
46
45
  private
47
46
 
48
47
  def number_of_items
49
- @config.lister.number_of_items
48
+ config.lister.number_of_items
50
49
  end
51
50
 
52
- def log_feud(response)
53
- @config.logger.call(
54
- code: :feud,
55
- theme: @theme[0..30],
56
- items: response['items'].map { |item| item[0..30] }
57
- )
51
+ def module_config
52
+ { llm: { recommended_model: 'claude-haiku-4-5' } }
58
53
  end
59
54
  end
60
55
  end
@@ -14,14 +14,9 @@ module ActiveGenie
14
14
  # @example Getting jury for technical content
15
15
  # Juries.call("Technical documentation about API design",
16
16
  # "Evaluate technical accuracy and clarity")
17
- # # => { jury1: "API Architect", jury2: "Technical Writer",
18
- # # jury3: "Developer Advocate", reasoning: "..." }
17
+ # # => [ "API Architect", "Technical Writer", "Developer Advocate" ]
19
18
  #
20
- class Juries
21
- def self.call(...)
22
- new(...).call
23
- end
24
-
19
+ class Juries < ActiveGenie::BaseModule
25
20
  # Initializes a new jury recommendation instance
26
21
  #
27
22
  # @param text [String] The text content to analyze for jury recommendations
@@ -30,7 +25,7 @@ module ActiveGenie
30
25
  def initialize(text, criteria, config: {})
31
26
  @text = text
32
27
  @criteria = criteria
33
- @config = ActiveGenie.configuration.merge(config)
28
+ super(config:)
34
29
  end
35
30
 
36
31
  def call
@@ -46,7 +41,10 @@ module ActiveGenie
46
41
  parameters: {
47
42
  type: 'object',
48
43
  properties: {
49
- reasoning: { type: 'string' },
44
+ why_these_juries: {
45
+ type: 'string',
46
+ description: 'A brief explanation of why these juries were chosen.'
47
+ },
50
48
  juries: {
51
49
  type: 'array',
52
50
  description: 'The list of best juries',
@@ -59,24 +57,28 @@ module ActiveGenie
59
57
  }
60
58
  }
61
59
 
62
- result = client.function_calling(
60
+ provider_response = ::ActiveGenie::Providers::UnifiedProvider.function_calling(
63
61
  messages,
64
62
  function,
65
- config: @config
63
+ config:
66
64
  )
67
65
 
68
- result['juries'] || []
66
+ ActiveGenie::Result.new(
67
+ data: provider_response['juries'] || [],
68
+ reasoning: provider_response['why_these_juries'],
69
+ metadata: provider_response
70
+ )
69
71
  end
70
72
 
71
73
  private
72
74
 
73
- def client
74
- ::ActiveGenie::Providers::UnifiedProvider
75
- end
76
-
77
75
  def prompt
78
76
  @prompt ||= File.read(File.join(__dir__, 'juries.prompt.md'))
79
77
  end
78
+
79
+ def module_config
80
+ { llm: { recommended_model: 'deepseek-chat' } }
81
+ end
80
82
  end
81
83
  end
82
84
  end
@@ -5,32 +5,24 @@ require 'fileutils'
5
5
 
6
6
  module ActiveGenie
7
7
  class Logger
8
- def initialize(log_config: nil)
9
- @log_config = log_config || ActiveGenie.configuration.log
10
- end
11
-
12
- def call(data)
13
- log = data.merge(@log_config.additional_context)
8
+ def call(data, config:)
9
+ log = data.merge(config.log.additional_context || {})
14
10
  .merge(
15
11
  timestamp: Time.now,
16
12
  process_id: Process.pid
17
13
  )
18
14
 
19
- persist!(log)
20
- output_call(log)
21
- call_observers(log)
15
+ persist!(log, config:)
16
+ output_call(log, config:)
17
+ call_observers(log, config:)
22
18
 
23
19
  log
24
20
  end
25
21
 
26
- def merge(log_config = nil)
27
- new(log_config:)
28
- end
29
-
30
22
  private
31
23
 
32
- def call_observers(log)
33
- Array(@log_config.observers).each do |observer|
24
+ def call_observers(log, config:)
25
+ Array(config.log.observers).each do |observer|
34
26
  next unless observer[:scope].all? { |key, value| log[key.to_sym] == value }
35
27
 
36
28
  observer[:observer]&.call(log)
@@ -39,9 +31,9 @@ module ActiveGenie
39
31
  end
40
32
  end
41
33
 
42
- def output_call(log)
43
- if @log_config.output
44
- @log_config.output&.call(log)
34
+ def output_call(log, config:)
35
+ if config.log.output
36
+ config.log.output&.call(log)
45
37
  else
46
38
  $stdout.puts log
47
39
  end
@@ -49,20 +41,16 @@ module ActiveGenie
49
41
  call(code: :output_error, error: e.message)
50
42
  end
51
43
 
52
- def persist!(log)
53
- file_path = log_to_file_path(log)
44
+ def persist!(log, config:)
45
+ file_path = if log.key?(:fine_tune) && log[:fine_tune]
46
+ config.log.fine_tune_file_path
47
+ else
48
+ config.log.file_path
49
+ end
54
50
  folder_path = File.dirname(file_path)
55
51
 
56
52
  FileUtils.mkdir_p(folder_path)
57
53
  File.write(file_path, "#{JSON.generate(log)}\n", mode: 'a')
58
54
  end
59
-
60
- def log_to_file_path(log)
61
- if log.key?(:fine_tune) && log[:fine_tune]
62
- @log_config.fine_tune_file_path
63
- else
64
- @log_config.file_path
65
- end
66
- end
67
55
  end
68
56
  end
@@ -30,7 +30,11 @@ module ActiveGenie
30
30
  temperature: @config.llm.temperature || 0
31
31
  }
32
32
 
33
- request(payload).dig('content', 0, 'input')
33
+ response = retry_with_backoff do
34
+ request(payload)
35
+ end
36
+
37
+ response.dig('content', 0, 'input')
34
38
  end
35
39
 
36
40
  ANTHROPIC_ENDPOINT = '/v1/messages'
@@ -55,7 +59,7 @@ module ActiveGenie
55
59
  def request(payload)
56
60
  response = post(url, payload, headers:)
57
61
 
58
- @config.logger.call(
62
+ ActiveGenie.logger.call(
59
63
  {
60
64
  code: :llm_usage,
61
65
  input_tokens: response.dig('usage', 'input_tokens'),
@@ -65,15 +69,17 @@ module ActiveGenie
65
69
  'output_tokens'),
66
70
  model: payload[:model],
67
71
  usage: response['usage']
68
- }
72
+ },
73
+ config: @config
69
74
  )
70
- @config.logger.call(
75
+ ActiveGenie.logger.call(
71
76
  {
72
77
  code: :function_calling,
73
78
  fine_tune: true,
74
79
  payload:,
75
80
  parsed_response: response.dig('content', 0, 'input')
76
- }
81
+ },
82
+ config: @config
77
83
  )
78
84
 
79
85
  response
@@ -84,7 +90,7 @@ module ActiveGenie
84
90
  end
85
91
 
86
92
  def model
87
- @config.llm.model || @config.providers.anthropic.tier_to_model(@config.llm.model_tier)
93
+ @config.llm.model
88
94
  end
89
95
 
90
96
  def headers
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'net/http'
4
+ require_relative '../errors/provider_server_error'
4
5
 
5
6
  module ActiveGenie
6
7
  module Providers
7
8
  class BaseProvider
8
9
  class ProviderUnknownError < StandardError; end
9
- class ProviderServerError < StandardError; end
10
10
 
11
11
  DEFAULT_HEADERS = {
12
12
  'Content-Type': 'application/json',
@@ -104,9 +104,7 @@ module ActiveGenie
104
104
  http.read_timeout = @config.llm.read_timeout || DEFAULT_TIMEOUT
105
105
  http.open_timeout = @config.llm.open_timeout || DEFAULT_OPEN_TIMEOUT
106
106
 
107
- retry_with_backoff do
108
- http.request(request)
109
- end
107
+ http.request(request)
110
108
  end
111
109
 
112
110
  # Apply headers to the request
@@ -154,7 +152,7 @@ module ActiveGenie
154
152
  #
155
153
  # @param details [Hash] Request and response details
156
154
  def log_request_details(uri:, request:, response:, start_time:, parsed_response:)
157
- @config.logger.call(
155
+ ActiveGenie.logger.call(
158
156
  {
159
157
  code: :http_request,
160
158
  uri: uri.to_s,
@@ -162,7 +160,7 @@ module ActiveGenie
162
160
  status: response.code,
163
161
  duration: Time.now - start_time,
164
162
  response_size: parsed_response.to_s.bytesize
165
- }
163
+ }, config: @config
166
164
  )
167
165
  end
168
166
 
@@ -172,23 +170,22 @@ module ActiveGenie
172
170
  retries = 0
173
171
 
174
172
  begin
175
- response = yield
176
-
177
- raise ProviderServerError, "Provider server error: #{response.code} - #{response.body}" if !response.is_a?(Net::HTTPSuccess) && response.code.to_i >= 500
178
-
179
- response
180
- rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, ProviderServerError => e
173
+ yield
174
+ rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, ActiveGenie::ProviderServerError,
175
+ JSON::ParserError => e
181
176
  raise if retries > max_retries
182
177
 
183
178
  sleep_time = retry_delay * (2**retries)
184
179
  retries += 1
185
180
 
186
- @config.logger.call(
187
- code: :retry_attempt,
188
- attempt: retries,
189
- max_retries: max_retries,
190
- delay: sleep_time,
191
- error: e.message
181
+ ActiveGenie.logger.call(
182
+ {
183
+ code: :retry_attempt,
184
+ attempt: retries,
185
+ max_retries:,
186
+ next_retry_in_seconds: sleep_time,
187
+ error: e.message
188
+ }, config: @config
192
189
  )
193
190
 
194
191
  sleep(sleep_time)
@@ -25,14 +25,14 @@ module ActiveGenie
25
25
  model:
26
26
  }
27
27
 
28
- response = request(payload)
29
-
30
- if response.nil? || response.keys.empty?
31
- raise InvalidResponseError,
32
- "Invalid response: #{response}"
28
+ response = retry_with_backoff do
29
+ request(payload)
33
30
  end
34
31
 
35
- @config.logger.call({ code: :function_calling, fine_tune: true, payload:, response: })
32
+ raise InvalidResponseError, "Invalid response: #{response}" if response.keys.empty?
33
+ raise InvalidResponseError, 'Invalid response: empty' if response.nil?
34
+
35
+ ActiveGenie.logger.call({ code: :function_calling, fine_tune: true, payload:, response: }, config: @config)
36
36
 
37
37
  response
38
38
  end
@@ -44,7 +44,7 @@ module ActiveGenie
44
44
 
45
45
  return nil if response.nil?
46
46
 
47
- @config.logger.call(
47
+ ActiveGenie.logger.call(
48
48
  {
49
49
  code: :llm_usage,
50
50
  input_tokens: response.dig('usage', 'prompt_tokens'),
@@ -52,11 +52,19 @@ module ActiveGenie
52
52
  total_tokens: response.dig('usage', 'total_tokens'),
53
53
  model:,
54
54
  usage: response['usage']
55
- }
55
+ }, config: @config
56
56
  )
57
57
 
58
- parsed_response = JSON.parse(response.dig('choices', 0, 'message', 'tool_calls', 0, 'function', 'arguments'))
58
+ parsed_response = JSON.parse(get_response_body(response))
59
59
  parsed_response['message'] || parsed_response
60
+ rescue JSON::ParserError
61
+ raise InvalidResponseError, "Invalid response: #{get_response_body(response)}"
62
+ end
63
+
64
+ def get_response_body(response)
65
+ response.dig('choices', 0, 'message', 'tool_calls', 0, 'function', 'arguments')
66
+ .gsub(', " "', '')
67
+ .strip
60
68
  end
61
69
 
62
70
  def function_to_tool(function)
@@ -74,7 +82,7 @@ module ActiveGenie
74
82
  end
75
83
 
76
84
  def model
77
- @config.llm.model || provider_config.tier_to_model(@config.llm.model_tier)
85
+ @config.llm.model
78
86
  end
79
87
 
80
88
  def url
@@ -29,12 +29,17 @@ module ActiveGenie
29
29
  }
30
30
  params = { key: provider_config.api_key }
31
31
 
32
- response = request(payload, params)
32
+ response = retry_with_backoff do
33
+ request(payload, params)
34
+ end
33
35
 
34
36
  json_string = response&.dig('candidates', 0, 'content', 'parts', 0, 'text')
35
37
  return nil if json_string.nil? || json_string.empty?
36
38
 
37
- @config.logger.call({ code: :function_calling, fine_tune: true, payload:, parsed_response: json_string })
39
+ ActiveGenie.logger.call(
40
+ { code: :function_calling, fine_tune: true, payload:, parsed_response: json_string },
41
+ config: @config
42
+ )
38
43
 
39
44
  normalize_response(json_string)
40
45
  end
@@ -50,7 +55,7 @@ module ActiveGenie
50
55
  def request(payload, params)
51
56
  response = post(url, payload, headers: DEFAULT_HEADERS, params:)
52
57
 
53
- @config.logger.call(
58
+ ActiveGenie.logger.call(
54
59
  {
55
60
  code: :llm_usage,
56
61
  input_tokens: response['usageMetadata']['promptTokenCount'] || 0,
@@ -58,7 +63,8 @@ module ActiveGenie
58
63
  total_tokens: response['usageMetadata']['totalTokenCount'] || (prompt_tokens + candidates_tokens),
59
64
  model:,
60
65
  usage: response['usageMetadata'] || {}
61
- }
66
+ },
67
+ config: @config
62
68
  )
63
69
 
64
70
  response
@@ -108,7 +114,7 @@ module ActiveGenie
108
114
  end
109
115
 
110
116
  def model
111
- @config.llm.model || provider_config.tier_to_model(@config.llm.model_tier)
117
+ @config.llm.model
112
118
  end
113
119
 
114
120
  def url
@@ -25,11 +25,13 @@ module ActiveGenie
25
25
  model:
26
26
  }
27
27
 
28
- response = request(payload)
28
+ response = retry_with_backoff do
29
+ request(payload)
30
+ end
29
31
 
30
32
  raise InvalidResponseError, "Invalid response: #{response}" if response.nil? || response.keys.empty?
31
33
 
32
- @config.logger.call({ code: :function_calling, fine_tune: true, payload:, response: })
34
+ ActiveGenie.logger.call({ code: :function_calling, fine_tune: true, payload:, response: }, config: @config)
33
35
 
34
36
  response
35
37
  end
@@ -37,11 +39,11 @@ module ActiveGenie
37
39
  private
38
40
 
39
41
  def request(payload)
40
- response = post(url, payload, headers: headers)
42
+ response = post(url, payload, headers:)
41
43
 
42
44
  return nil if response.nil?
43
45
 
44
- @config.logger.call(
46
+ ActiveGenie.logger.call(
45
47
  {
46
48
  code: :llm_usage,
47
49
  input_tokens: response.dig('usage', 'prompt_tokens'),
@@ -49,7 +51,7 @@ module ActiveGenie
49
51
  total_tokens: response.dig('usage', 'total_tokens'),
50
52
  model:,
51
53
  usage: response['usage']
52
- }
54
+ }, config: @config
53
55
  )
54
56
 
55
57
  parsed_response = JSON.parse(response.dig('choices', 0, 'message', 'tool_calls', 0, 'function', 'arguments'))
@@ -71,7 +73,7 @@ module ActiveGenie
71
73
  end
72
74
 
73
75
  def model
74
- @config.llm.model || provider_config.tier_to_model(@config.llm.model_tier)
76
+ @config.llm.model
75
77
  end
76
78
 
77
79
  def url
@@ -5,6 +5,8 @@ require_relative 'anthropic_provider'
5
5
  require_relative 'google_provider'
6
6
  require_relative 'deepseek_provider'
7
7
  require_relative '../errors/invalid_provider_error'
8
+ require_relative '../errors/invalid_model_error'
9
+ require_relative '../errors/without_available_provider_error'
8
10
 
9
11
  module ActiveGenie
10
12
  module Providers
@@ -18,10 +20,13 @@ module ActiveGenie
18
20
  }.freeze
19
21
 
20
22
  def function_calling(messages, function, config: {})
21
- provider_name = config.llm.provider_name || config.providers.default
22
- provider = PROVIDER_NAME_TO_PROVIDER[provider_name.to_sym]
23
+ model, provider_name = model_and_provider_by(config)
23
24
 
24
- raise ActiveGenie::InvalidProviderError, provider_name if provider.nil?
25
+ provider = PROVIDER_NAME_TO_PROVIDER[provider_name&.to_sym]
26
+
27
+ raise ActiveGenie::WithoutAvailableProviderError if provider.nil?
28
+
29
+ config.llm.model = model
25
30
 
26
31
  response = provider.new(config).function_calling(messages, function)
27
32
 
@@ -30,6 +35,56 @@ module ActiveGenie
30
35
 
31
36
  private
32
37
 
38
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
39
+ def model_and_provider_by(config)
40
+ model, provider_name = explicit_choice(config)
41
+ model, provider_name = global_default(config) if model.nil? && provider_name.nil?
42
+ model, provider_name = module_recommendation(config) if model.nil? && provider_name.nil?
43
+
44
+ model, provider_name = infer_from_partial(config, model, provider_name) if model.nil? || provider_name.nil?
45
+ model, provider_name = any_available(config) if model.nil? || provider_name.nil?
46
+
47
+ [model, provider_name]
48
+ end
49
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
50
+
51
+ def explicit_choice(config)
52
+ model = config.llm.model
53
+ provider_name = config.providers.default
54
+
55
+ [model, provider_name]
56
+ end
57
+
58
+ def global_default(config)
59
+ provider_name = config.providers.default
60
+
61
+ [nil, provider_name]
62
+ end
63
+
64
+ def module_recommendation(config)
65
+ model = config.llm.recommended_model
66
+ provider_name = config.providers.provider_name_by_model(model) if model
67
+
68
+ return nil if model.nil? || provider_name.nil?
69
+
70
+ [model, provider_name]
71
+ end
72
+
73
+ def infer_from_partial(config, model, provider_name)
74
+ provider_name ||= config.providers.provider_name_by_model(model) if model
75
+ model ||= config.providers.valid[provider_name.to_sym]&.default_model if provider_name
76
+
77
+ [model, provider_name]
78
+ end
79
+
80
+ def any_available(config)
81
+ provider = config.providers.valid.first
82
+ provider_name = provider&.first
83
+ model = provider&.last&.default_model
84
+
85
+ [model, provider_name]
86
+ end
87
+
33
88
  def normalize_response(response)
34
89
  response.each do |key, value|
35
90
  response[key] = nil if ['null', 'none', 'undefined', '', 'unknown',