diff --git a/CHANGELOG.md b/CHANGELOG.md index 390d81d..fe6846e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.7] - 2026-03-02 + +### Changed + +- **Default API key lookup order**: `AI::Chat.new` and `AI::Chat.generate_schema!` now look for `AICHAT_API_KEY` first, then fall back to `OPENAI_API_KEY` when the first value is missing or empty. + +- **Explicit API key override behavior**: Passing `api_key:` still takes highest precedence, and passing `api_key_env_var:` still uses that environment variable exactly. + +### Added + +- **Unit coverage for API key precedence**: Added tests covering default env order, empty `AICHAT_API_KEY` fallback, explicit `api_key_env_var:`, and explicit `api_key:` behavior. + +### Updated + +- **Integration test setup**: Integration tests now run when either `AICHAT_API_KEY` or `OPENAI_API_KEY` is present. + +- **Documentation**: README now documents the new default key lookup order and updates the direct `OpenAI::Client` example to match. + ## [0.5.6] - 2026-03-02 ### Changed diff --git a/Gemfile.lock b/Gemfile.lock index 4a01d5b..e53e2f1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - ai-chat (0.5.6) + ai-chat (0.5.7) amazing_print (~> 2.0) base64 (~> 0.1, > 0.1.1) json (~> 2.0) diff --git a/README.md b/README.md index 3318346..e0fb024 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ See [OpenAI's model documentation](https://platform.openai.com/docs/models) for ### API key -The gem by default looks for an environment variable called `OPENAI_API_KEY` and uses that if it finds it. +The gem by default looks for `AICHAT_API_KEY` first. If that is missing (or empty), it falls back to `OPENAI_API_KEY`. You can specify a different environment variable name: @@ -404,7 +404,7 @@ AI::Chat.generate_schema!("A user profile with name (required), email (required) This method returns a String containing the JSON schema. The JSON schema also writes (or overwrites) to `schema.json` at the root of the project. -Similar to generating messages with `AI::Chat` objects, this class method will assume that you have an API key called `OPENAI_API_KEY` defined. You can also pass the API key directly or choose a different environment variable key for it to use. +Similar to generating messages with `AI::Chat` objects, this class method will look for `AICHAT_API_KEY` first, then fall back to `OPENAI_API_KEY` if needed. You can also pass the API key directly or choose a different environment variable key for it to use. ```rb # Passing the API key directly @@ -748,7 +748,9 @@ This is particularly useful for background mode workflows. If you want to retrie ```ruby require "openai" -client = OpenAI::Client.new(api_key: ENV.fetch("OPENAI_API_KEY")) +api_key = ENV["AICHAT_API_KEY"] +api_key = ENV.fetch("OPENAI_API_KEY") if api_key.nil? || api_key.empty? +client = OpenAI::Client.new(api_key: api_key) response_id = "resp_abc123..." # e.g., load from your database response = client.responses.retrieve(response_id) diff --git a/ai-chat.gemspec b/ai-chat.gemspec index 23b64a5..33a881a 100644 --- a/ai-chat.gemspec +++ b/ai-chat.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |spec| spec.name = "ai-chat" - spec.version = "0.5.6" + spec.version = "0.5.7" spec.authors = ["Raghu Betina", "Jelani Woods"] spec.email = ["raghu@firstdraft.com", "jelani@firstdraft.com"] spec.homepage = "https://github.com/firstdraft/ai-chat" diff --git a/lib/ai/chat.rb b/lib/ai/chat.rb index 087c9a7..b9fb108 100644 --- a/lib/ai/chat.rb +++ b/lib/ai/chat.rb @@ -23,8 +23,8 @@ class Chat BASE_PROXY_URL = "https://prepend.me/api.openai.com/v1" - def initialize(api_key: nil, api_key_env_var: "OPENAI_API_KEY") - @api_key = api_key || ENV.fetch(api_key_env_var) + def initialize(api_key: nil, api_key_env_var: nil) + @api_key = self.class.resolve_api_key(api_key: api_key, api_key_env_var: api_key_env_var) @proxy = ENV["AICHAT_PROXY"] == "true" @messages = [] @reasoning_effort = nil @@ -39,8 +39,8 @@ def initialize(api_key: nil, api_key_env_var: "OPENAI_API_KEY") @verbosity = :medium end - def self.generate_schema!(description, location: "schema.json", api_key: nil, api_key_env_var: "OPENAI_API_KEY", proxy: nil) - api_key ||= ENV.fetch(api_key_env_var) + def self.generate_schema!(description, location: "schema.json", api_key: nil, api_key_env_var: nil, proxy: nil) + api_key = resolve_api_key(api_key: api_key, api_key_env_var: api_key_env_var) proxy = ENV["AICHAT_PROXY"] == "true" if proxy.nil? prompt_path = File.expand_path("../prompts/schema_generator.md", __dir__) system_prompt = File.read(prompt_path) @@ -73,6 +73,16 @@ def self.generate_schema!(description, location: "schema.json", api_key: nil, ap content end + def self.resolve_api_key(api_key: nil, api_key_env_var: nil) + return api_key if api_key + return ENV.fetch(api_key_env_var) if api_key_env_var + + aichat_api_key = ENV["AICHAT_API_KEY"] + return aichat_api_key if aichat_api_key && !aichat_api_key.empty? + + ENV.fetch("OPENAI_API_KEY") + end + # :reek:TooManyStatements # :reek:NilCheck def add(content, role: "user", response: nil, status: nil, image: nil, images: nil, file: nil, files: nil) diff --git a/spec/integration/ai_chat_integration_spec.rb b/spec/integration/ai_chat_integration_spec.rb index fb13728..f288ee5 100644 --- a/spec/integration/ai_chat_integration_spec.rb +++ b/spec/integration/ai_chat_integration_spec.rb @@ -225,12 +225,12 @@ end describe "API key configuration" do - it "uses OPENAI_API_KEY environment variable by default" do + it "uses the default API key env lookup by default" do expect { AI::Chat.new }.not_to raise_error end it "accepts a custom environment variable name" do - ENV["CUSTOM_OPENAI_KEY"] = ENV["OPENAI_API_KEY"] + ENV["CUSTOM_OPENAI_KEY"] = ENV["AICHAT_API_KEY"] || ENV["OPENAI_API_KEY"] chat = AI::Chat.new(api_key_env_var: "CUSTOM_OPENAI_KEY") chat.user("Hi") @@ -241,7 +241,7 @@ end it "accepts an API key directly" do - chat = AI::Chat.new(api_key: ENV["OPENAI_API_KEY"]) + chat = AI::Chat.new(api_key: ENV["AICHAT_API_KEY"] || ENV["OPENAI_API_KEY"]) chat.user("Hi") expect { chat.generate! }.not_to raise_error diff --git a/spec/support/integration_helper.rb b/spec/support/integration_helper.rb index 4bb57db..708460d 100644 --- a/spec/support/integration_helper.rb +++ b/spec/support/integration_helper.rb @@ -2,8 +2,8 @@ RSpec.configure do |config| config.before(:each, :integration) do - if ENV["OPENAI_API_KEY"].nil? - skip "Integration tests require OPENAI_API_KEY environment variable" + if ENV["AICHAT_API_KEY"].to_s.empty? && ENV["OPENAI_API_KEY"].to_s.empty? + skip "Integration tests require AICHAT_API_KEY or OPENAI_API_KEY environment variable" end end end diff --git a/spec/unit/chat_spec.rb b/spec/unit/chat_spec.rb index 5689ee3..79b675d 100644 --- a/spec/unit/chat_spec.rb +++ b/spec/unit/chat_spec.rb @@ -32,7 +32,66 @@ def schema_client_double around do |example| with_env_var("AICHAT_PROXY", nil) do - example.run + with_env_var("AICHAT_API_KEY", nil) do + example.run + end + end + end + + describe "api key defaults" do + it "uses AICHAT_API_KEY before OPENAI_API_KEY by default" do + with_env_var("AICHAT_API_KEY", "aichat-key") do + with_env_var("OPENAI_API_KEY", "openai-key") do + client_double = instance_double(OpenAI::Client) + expect(OpenAI::Client).to receive(:new).with(api_key: "aichat-key").and_return(client_double) + + AI::Chat.new + end + end + end + + it "falls back to OPENAI_API_KEY when AICHAT_API_KEY is missing" do + with_env_var("AICHAT_API_KEY", nil) do + with_env_var("OPENAI_API_KEY", "openai-key") do + client_double = instance_double(OpenAI::Client) + expect(OpenAI::Client).to receive(:new).with(api_key: "openai-key").and_return(client_double) + + AI::Chat.new + end + end + end + + it "treats empty AICHAT_API_KEY as missing and falls back to OPENAI_API_KEY" do + with_env_var("AICHAT_API_KEY", "") do + with_env_var("OPENAI_API_KEY", "openai-key") do + client_double = instance_double(OpenAI::Client) + expect(OpenAI::Client).to receive(:new).with(api_key: "openai-key").and_return(client_double) + + AI::Chat.new + end + end + end + + it "uses only the explicitly provided api_key_env_var when present" do + with_env_var("AICHAT_API_KEY", "aichat-key") do + with_env_var("CUSTOM_OPENAI_KEY", "custom-key") do + client_double = instance_double(OpenAI::Client) + expect(OpenAI::Client).to receive(:new).with(api_key: "custom-key").and_return(client_double) + + AI::Chat.new(api_key_env_var: "CUSTOM_OPENAI_KEY") + end + end + end + + it "uses explicit api_key before env vars" do + with_env_var("AICHAT_API_KEY", "aichat-key") do + with_env_var("OPENAI_API_KEY", "openai-key") do + client_double = instance_double(OpenAI::Client) + expect(OpenAI::Client).to receive(:new).with(api_key: "direct-key").and_return(client_double) + + AI::Chat.new(api_key: "direct-key") + end + end end end @@ -103,6 +162,50 @@ def schema_client_double end describe ".generate_schema!" do + it "uses AICHAT_API_KEY before OPENAI_API_KEY by default" do + with_env_var("AICHAT_API_KEY", "aichat-key") do + with_env_var("OPENAI_API_KEY", "openai-key") do + client_double = schema_client_double + expect(OpenAI::Client).to receive(:new).with(api_key: "aichat-key").and_return(client_double) + + AI::Chat.generate_schema!("A tiny schema", location: false) + end + end + end + + it "falls back to OPENAI_API_KEY when AICHAT_API_KEY is empty" do + with_env_var("AICHAT_API_KEY", "") do + with_env_var("OPENAI_API_KEY", "openai-key") do + client_double = schema_client_double + expect(OpenAI::Client).to receive(:new).with(api_key: "openai-key").and_return(client_double) + + AI::Chat.generate_schema!("A tiny schema", location: false) + end + end + end + + it "uses only the explicitly provided api_key_env_var when present" do + with_env_var("AICHAT_API_KEY", "aichat-key") do + with_env_var("CUSTOM_OPENAI_KEY", "custom-key") do + client_double = schema_client_double + expect(OpenAI::Client).to receive(:new).with(api_key: "custom-key").and_return(client_double) + + AI::Chat.generate_schema!("A tiny schema", location: false, api_key_env_var: "CUSTOM_OPENAI_KEY") + end + end + end + + it "uses explicit api_key before env vars" do + with_env_var("AICHAT_API_KEY", "aichat-key") do + with_env_var("OPENAI_API_KEY", "openai-key") do + client_double = schema_client_double + expect(OpenAI::Client).to receive(:new).with(api_key: "direct-key").and_return(client_double) + + AI::Chat.generate_schema!("A tiny schema", location: false, api_key: "direct-key") + end + end + end + it "uses env proxy default when proxy keyword is omitted" do with_env_var("AICHAT_PROXY", "true") do client_double = schema_client_double