Module: Aireview::ConfigFallbacks

Included in:
Config
Defined in:
lib/aireview/config_fallbacks.rb

Overview

Reserves for when the primary model is down or a key is out of quota: the routing plan (see StageChains and ModelPool) and the list of keys per provider. Models are set in .aireview.yml or the environment, keys only in the environment.

Constant Summary collapse

KEYLESS_PROVIDERS =
%w[ollama].freeze
DEFAULT_TIME_BUDGET =
1_800
DEFAULT_OVERLOADED_QUARANTINE =
120
SELF_HOSTED_PLACEHOLDER_KEY =

The keys a model is called with. A server of your own (a model with api_base) never gets the provider's key — OPENAI_API_KEY must not leave for a self-hosted server — but LLM_API_KEY, or a placeholder for a server without auth, since the client wants some key.

'no-key'

Instance Method Summary collapse

Instance Method Details

#api_key_counts(stages) ⇒ Object

Only the number of keys per provider, for --dry-run; the values never leave. Servers of your own are not counted: they take LLM_API_KEY.



64
65
66
67
68
# File 'lib/aireview/config_fallbacks.rb', line 64

def api_key_counts(stages)
  providers = stages.flat_map { |stage| stage_chain(stage).reject(&:api_base).map(&:provider) }.uniq
  providers.reject { |provider| KEYLESS_PROVIDERS.include?(provider.to_s) }
    .to_h { |provider| [provider, provider_api_keys(provider).size] }
end

#candidate_api_keys(candidate) ⇒ Object



36
37
38
39
40
41
# File 'lib/aireview/config_fallbacks.rb', line 36

def candidate_api_keys(candidate)
  return provider_api_keys(candidate.provider) unless candidate.api_base
  return [nil] if KEYLESS_PROVIDERS.include?(candidate.provider.to_s)

  [Aireview::Utils.presence(llm_api_key) || SELF_HOSTED_PLACEHOLDER_KEY]
end

#fallback_names(stage) ⇒ Object



58
59
60
# File 'lib/aireview/config_fallbacks.rb', line 58

def fallback_names(stage)
  stage_chain(stage).drop(1).map(&:to_s)
end

#fallbacks_disabled? ⇒ Boolean

Returns:

  • (Boolean)


54
55
56
# File 'lib/aireview/config_fallbacks.rb', line 54

def fallbacks_disabled?
  @data['fallbacks_disabled'] == true
end

#llm_time_budget ⇒ Object

The ceiling for all LLM requests of the run, pauses between attempts included: the chain of reserves must not eat the whole CI job.



72
73
74
# File 'lib/aireview/config_fallbacks.rb', line 72

def llm_time_budget
  positive_integer!(dig('llm', 'time_budget') || DEFAULT_TIME_BUDGET, 'llm.time_budget')
end

#overloaded_quarantine ⇒ Object

For how many seconds an overloaded or hung model is skipped before the router tries it again.



78
79
80
81
# File 'lib/aireview/config_fallbacks.rb', line 78

def overloaded_quarantine
  positive_integer!(dig('llm', 'overloaded_quarantine') || DEFAULT_OVERLOADED_QUARANTINE,
                    'llm.overloaded_quarantine')
end

#provider_api_keys(provider) ⇒ Object

Keys in order of preference; a single nil for a keyless provider, so that walking the chain does not depend on the provider.



45
46
47
48
49
50
51
52
# File 'lib/aireview/config_fallbacks.rb', line 45

def provider_api_keys(provider)
  provider = provider.to_s
  return [nil] if KEYLESS_PROVIDERS.include?(provider)

  keys = Array(@data["#{provider}_api_keys"]).compact
  keys = [provider_api_key(provider)].compact if keys.empty?
  fallbacks_disabled? ? keys.first(1) : keys
end

#require_llm_configuration!(critique: true) ⇒ Object

The plan is built whole (pool and critique policy validated) before the first request, not in Critique after a paid-for Generate. Keys are required for the stages that need an LLM: generate on Ollama with Jev as the critic and no fallback needs no Gemini key.

Raises:



99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/aireview/config_fallbacks.rb', line 99

def require_llm_configuration!(critique: true)
  require_models!(critique: critique)
  require_jev!(critique: critique)
  routing

  missing_keys = llm_stages(critique: critique).flat_map do |stage|
    providers = stage_chain(stage).reject(&:api_base).map(&:provider).uniq
    providers.reject { |provider| provider_keys_present?(provider) }
      .map { |provider| "#{stage}: API key is required for provider #{provider.inspect}" }
  end
  raise ConfigError, missing_keys.join(', ') unless missing_keys.empty?
end

#require_models!(critique: true) ⇒ Object

critique — the run has a critique (false with --no-critique); only the stages that need an LLM in such a run are required (see llm_stages).

Raises:



85
86
87
88
89
90
91
92
93
# File 'lib/aireview/config_fallbacks.rb', line 85

def require_models!(critique: true)
  stages = llm_stages(critique: critique)
  missing = []
  missing << 'llm.generate.model (or LLM_GENERATE_MODEL)' if Aireview::Utils.blank?(generate_model)
  if stages.include?('critique') && Aireview::Utils.blank?(critique_model)
    missing << 'llm.critique.model (or LLM_CRITIQUE_MODEL)'
  end
  raise ConfigError, "LLM models are required: #{missing.join(', ')}" unless missing.empty?
end

#routing ⇒ Object

The routing plan is built once: a shared pool when llm.models is set, independent stage chains otherwise. A stage with a model of its own inside the pool is an independent chain.



22
23
24
# File 'lib/aireview/config_fallbacks.rb', line 22

def routing
  @routing ||= build_routing
end

#stage_chain(stage) ⇒ Object



26
27
28
# File 'lib/aireview/config_fallbacks.rb', line 26

def stage_chain(stage)
  routing.chain(stage)
end