Module: AiCli::Helpers::Llm

Defined in:
lib/aicli/helpers/llm.rb

Constant Summary collapse

PROVIDERS =
%w[openai anthropic].freeze
DEFAULT_MODELS =
{
  'openai' => 'gpt-4o-mini',
  'anthropic' => 'claude-sonnet-4-6'
}.freeze

Class Method Summary collapse

Class Method Details

.build_chat(config) ⇒ Object



37
38
39
40
41
42
43
44
45
46
# File 'lib/aicli/helpers/llm.rb', line 37

def build_chat(config)
  provider, model = resolve_provider_and_model(config)
  ensure_credentials!(config, provider)

  configure!(config.merge('PROVIDER' => provider, 'MODEL' => model))

  opts = { model: model, provider: provider.to_sym }
  opts[:assume_model_exists] = true unless model_known?(model, provider)
  RubyLLM.chat(**opts)
end

.configure!(config) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/aicli/helpers/llm.rb', line 17

def configure!(config)
  provider = normalize_provider(config['PROVIDER'])
  ensure_credentials!(config, provider)

  RubyLLM.configure do |c|
    openai_key = config['OPENAI_KEY'].to_s
    anthropic_key = config['ANTHROPIC_KEY'].to_s
    endpoint = config['OPENAI_API_ENDPOINT'].to_s

    c.openai_api_key = openai_key unless openai_key.empty?
    c.anthropic_api_key = anthropic_key unless anthropic_key.empty?

    if !endpoint.empty? && endpoint != 'https://api.openai.com/v1'
      c.openai_api_base = endpoint.sub(%r{/+\z}, '')
    end
  end

  provider
end

.default_model_for(provider) ⇒ Object



89
90
91
# File 'lib/aicli/helpers/llm.rb', line 89

def default_model_for(provider)
  DEFAULT_MODELS.fetch(normalize_provider(provider), DEFAULT_MODELS['openai'])
end

.ensure_credentials!(config, provider = nil) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/aicli/helpers/llm.rb', line 98

def ensure_credentials!(config, provider = nil)
  provider = normalize_provider(provider || config['PROVIDER'])

  case provider
  when 'anthropic'
    if config['ANTHROPIC_KEY'].to_s.empty?
      raise KnownError,
            "Please set your Anthropic API key via `#{Constants::COMMAND_NAME} config set ANTHROPIC_KEY=<your token>`"
    end
  else
    if config['OPENAI_KEY'].to_s.empty?
      raise KnownError,
            "Please set your OpenAI API key via `#{Constants::COMMAND_NAME} config set OPENAI_KEY=<your token>`"
    end
  end
end

.list_chat_models(provider) ⇒ Object



79
80
81
82
83
84
85
86
87
# File 'lib/aicli/helpers/llm.rb', line 79

def list_chat_models(provider)
  provider = normalize_provider(provider)

  RubyLLM.models.chat_models
         .by_provider(provider.to_sym)
         .select { |m| text_chat_model?(m) }
         .sort_by { |m| [m.name.to_s.downcase, m.id] }
         .map { |m| { 'id' => m.id, 'name' => m.name.to_s } }
end

.model_known?(model, provider) ⇒ Boolean

Returns:

  • (Boolean)


115
116
117
118
119
120
# File 'lib/aicli/helpers/llm.rb', line 115

def model_known?(model, provider)
  RubyLLM.models.find(model, provider.to_sym)
  true
rescue StandardError
  false
end

.normalize_provider(provider) ⇒ Object



93
94
95
96
# File 'lib/aicli/helpers/llm.rb', line 93

def normalize_provider(provider)
  value = provider.to_s.downcase
  PROVIDERS.include?(value) ? value : 'openai'
end

.provider_for_model(model_id) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/aicli/helpers/llm.rb', line 66

def provider_for_model(model_id)
  return nil if model_id.to_s.empty?

  info = RubyLLM.models.find(model_id)
  normalize_provider(info.provider)
rescue StandardError
  # Try each known provider explicitly (aliases can be provider-specific).
  PROVIDERS.each do |provider|
    return provider if model_known?(model_id, provider)
  end
  nil
end

.resolve_provider_and_model(config) ⇒ Object

Returns [provider, model], fixing mismatches against the RubyLLM registry.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/aicli/helpers/llm.rb', line 49

def resolve_provider_and_model(config)
  provider = normalize_provider(config['PROVIDER'])
  model = config['MODEL'].to_s
  model = default_model_for(provider) if model.empty?

  model_provider = provider_for_model(model)
  if model_provider && model_provider != provider
    # Prefer the provider that actually owns the selected model.
    provider = model_provider
  elsif model_provider.nil? && !model_known?(model, provider)
    # Unknown id for this provider: fall back to a safe default.
    model = default_model_for(provider)
  end

  [provider, model]
end

.text_chat_model?(model) ⇒ Boolean

Returns:

  • (Boolean)


122
123
124
125
126
127
# File 'lib/aicli/helpers/llm.rb', line 122

def text_chat_model?(model)
  outputs = Array(model.modalities&.output).map(&:to_s)
  return false unless outputs.include?('text')

  !model.id.match?(/tts|transcribe|realtime|whisper|embedding|moderation|dall-e|imagen|search-preview|babbage|davinci|ada-|curie|instruct/i)
end