From 6cfda555f5f5044ae84b7d396e29517224626623 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Mon, 2 Mar 2026 04:01:33 -0600 Subject: [PATCH] Prefer AICHAT_API_KEY in default lookup Change default key resolution in AI::Chat and generate_schema! to check AICHAT_API_KEY first and fall back to OPENAI_API_KEY when the first value is missing or empty. Keep explicit configuration behavior unchanged: - api_key: still has highest precedence. - api_key_env_var: still targets exactly one env var. Add unit coverage for precedence and fallback rules, update integration test setup to accept either env var, and refresh README examples to match runtime behavior. Bump gem version to 0.5.7 and add a changelog entry. --- CHANGELOG.md | 18 ++++ Gemfile.lock | 2 +- README.md | 8 +- ai-chat.gemspec | 2 +- lib/ai/chat.rb | 18 +++- spec/integration/ai_chat_integration_spec.rb | 6 +- spec/support/integration_helper.rb | 4 +- spec/unit/chat_spec.rb | 105 ++++++++++++++++++- 8 files changed, 148 insertions(+), 15 deletions(-) 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