Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion ai-chat.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
18 changes: 14 additions & 4 deletions lib/ai/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions spec/integration/ai_chat_integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat empty AICHAT_API_KEY as missing in integration key setup

Using ENV["AICHAT_API_KEY"] || ENV["OPENAI_API_KEY"] does not implement the intended fallback when AICHAT_API_KEY is present but empty, because Ruby treats "" as truthy. In environments where AICHAT_API_KEY="" and OPENAI_API_KEY is valid, this assigns an empty key to CUSTOM_OPENAI_KEY, so the custom-env integration test uses an invalid credential and can fail even though the runtime default lookup is designed to fall back to OPENAI_API_KEY for empty values.

Useful? React with 👍 / 👎.


chat = AI::Chat.new(api_key_env_var: "CUSTOM_OPENAI_KEY")
chat.user("Hi")
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions spec/support/integration_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
105 changes: 104 additions & 1 deletion spec/unit/chat_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Loading