diff --git a/lib/locallingo/providers/ruby_llm.rb b/lib/locallingo/providers/ruby_llm.rb index cf02751..3be1077 100644 --- a/lib/locallingo/providers/ruby_llm.rb +++ b/lib/locallingo/providers/ruby_llm.rb @@ -54,6 +54,7 @@ def ensure_credentials! # the model and return the parsed JSON object as a Hash. def chat(model:, instructions:, payload:) require "ruby_llm" + configure_credentials! conversation = ::RubyLLM.chat( model:, @@ -64,6 +65,26 @@ def chat(model:, instructions:, payload:) response = conversation.ask(JSON.pretty_generate(payload)) JsonExtraction.extract_object(response.content) end + + private + + # RubyLLM does not read provider API keys from ENV on its own, so a + # standalone CLI run (no Rails initializer to call RubyLLM.configure) + # would raise "Missing configuration for ". Fill the + # provider's key from ENV — unless the host app already configured + # one, which always wins. + def configure_credentials! + env = CREDENTIAL_ENV[provider] + return unless env + + setting = "#{provider}_api_key" + config = ::RubyLLM.config + return unless config.respond_to?(setting) && config.respond_to?("#{setting}=") + return unless config.public_send(setting).to_s.strip.empty? + + key = ENV.fetch(env, "") + config.public_send("#{setting}=", key) unless key.strip.empty? + end end end end diff --git a/spec/locallingo/providers/ruby_llm_spec.rb b/spec/locallingo/providers/ruby_llm_spec.rb new file mode 100644 index 0000000..171bb16 --- /dev/null +++ b/spec/locallingo/providers/ruby_llm_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "ruby_llm" + +RSpec.describe Locallingo::Providers::RubyLLM do + subject(:provider) { described_class.new(provider: :anthropic) } + + describe "#chat" do + let(:conversation) { double } + let(:response) { double(content: '{"greeting":"hallo"}') } + + before do + allow(RubyLLM).to receive(:chat).and_return(conversation) + allow(conversation).to receive_messages(with_instructions: conversation, ask: response) + end + + around do |example| + original = RubyLLM.config.anthropic_api_key + example.run + ensure + RubyLLM.config.anthropic_api_key = original + end + + it "configures the provider API key from ENV before chatting" do + RubyLLM.config.anthropic_api_key = nil + stub_const("ENV", ENV.to_h.merge("ANTHROPIC_API_KEY" => "env-key-123")) + + result = provider.chat(model: "claude-x", instructions: "translate", payload: { "greeting" => "hello" }) + + expect(RubyLLM.config.anthropic_api_key).to eq("env-key-123") + expect(result).to eq({ "greeting" => "hallo" }) + end + + it "does not clobber a key the host app already configured" do + RubyLLM.config.anthropic_api_key = "explicit-app-key" + stub_const("ENV", ENV.to_h.merge("ANTHROPIC_API_KEY" => "env-key-123")) + + provider.chat(model: "claude-x", instructions: "translate", payload: { "greeting" => "hello" }) + + expect(RubyLLM.config.anthropic_api_key).to eq("explicit-app-key") + end + + it "leaves configuration untouched when the ENV var is absent" do + RubyLLM.config.anthropic_api_key = nil + stub_const("ENV", ENV.to_h.except("ANTHROPIC_API_KEY")) + + provider.chat(model: "claude-x", instructions: "translate", payload: { "greeting" => "hello" }) + + expect(RubyLLM.config.anthropic_api_key).to be_nil + end + end +end