From 7c370d4288cb3d6789e7e47b858e1748421f432e Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Mon, 2 Mar 2026 03:44:15 -0600 Subject: [PATCH] Default proxy mode from AICHAT_PROXY env Enable proxy by default when AICHAT_PROXY is exactly "true". This applies to both AI::Chat.new and AI::Chat.generate_schema! when proxy: is omitted. Explicit settings still take precedence: chat.proxy= and generate_schema!(proxy: ...) override env defaults. Add unit tests for env parsing, constructor client wiring, and generate_schema! precedence behavior. Add README docs for env usage and precedence semantics. Bump version to 0.5.6 and add changelog entry for the release. --- CHANGELOG.md | 16 +++++ Gemfile.lock | 2 +- README.md | 16 +++++ ai-chat.gemspec | 2 +- lib/ai/chat.rb | 9 ++- spec/unit/chat_spec.rb | 134 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 174 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e70fa39..390d81d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ 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.6] - 2026-03-02 + +### Changed + +- **Proxy default from env**: `AI::Chat.new` now enables proxy mode by default when `AICHAT_PROXY` is exactly `"true"`. + +- **Schema generation proxy default**: `AI::Chat.generate_schema!` now uses the same `AICHAT_PROXY` default when `proxy:` is omitted. + +- **Explicit override precedence**: Explicit `chat.proxy = ...` and `generate_schema!(..., proxy: ...)` continue to override env defaults. + +### Added + +- **Unit coverage for proxy defaults**: Added tests for env parsing (`"true"` exact match), explicit override behavior, and `generate_schema!` precedence. + +- **Proxy env documentation**: README now documents `AICHAT_PROXY` behavior for both chat generation and schema generation. + ## [0.5.5] - 2026-02-10 ### Fixed diff --git a/Gemfile.lock b/Gemfile.lock index 74d2c22..4a01d5b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - ai-chat (0.5.5) + ai-chat (0.5.6) amazing_print (~> 2.0) base64 (~> 0.1, > 0.1.1) json (~> 2.0) diff --git a/README.md b/README.md index 10ca111..3318346 100644 --- a/README.md +++ b/README.md @@ -414,6 +414,14 @@ AI::Chat.generate_schema!("A user with full name (required), first_name (require AI::Chat.generate_schema!("A user with full name (required), first_name (required), and last_name (required).", api_key_env_var: "CUSTOM_KEY") ``` +`generate_schema!` also follows proxy defaults from the `AICHAT_PROXY` environment variable. Proxy is enabled only when `AICHAT_PROXY` is exactly `"true"`. + +```bash +export AICHAT_PROXY=true +``` + +If you pass `proxy: true` or `proxy: false`, that explicit value overrides the env default. + You can choose a location for the schema to save by using the `location` keyword argument. ```rb @@ -622,6 +630,14 @@ puts chat.last[:content] # => "Once upon a time..." ``` +You can also default proxy mode from the environment for both `AI::Chat.new` and `AI::Chat.generate_schema!`: + +```bash +export AICHAT_PROXY=true +``` + +Proxy is enabled only when `AICHAT_PROXY` is exactly `"true"`. Any other value (including `"TRUE"` or `"1"`) leaves proxy disabled unless you explicitly set `chat.proxy = true` or pass `proxy: true`. + When proxy is enabled, **you must use the API key provided by prepend.me** in place of a real OpenAI API key. Refer to [the section on API keys](#api-key) for options on how to set your key. ## Building Conversations Without API Calls diff --git a/ai-chat.gemspec b/ai-chat.gemspec index cd462eb..23b64a5 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.5" + spec.version = "0.5.6" 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 b2b7f91..087c9a7 100644 --- a/lib/ai/chat.rb +++ b/lib/ai/chat.rb @@ -25,11 +25,13 @@ class Chat def initialize(api_key: nil, api_key_env_var: "OPENAI_API_KEY") @api_key = api_key || ENV.fetch(api_key_env_var) - @proxy = false + @proxy = ENV["AICHAT_PROXY"] == "true" @messages = [] @reasoning_effort = nil @model = "gpt-5.2" - @client = OpenAI::Client.new(api_key: @api_key) + client_options = {api_key: @api_key} + client_options[:base_url] = BASE_PROXY_URL if @proxy + @client = OpenAI::Client.new(**client_options) @last_response_id = nil @image_generation = false @image_folder = "./images" @@ -37,8 +39,9 @@ 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: false) + 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) + proxy = ENV["AICHAT_PROXY"] == "true" if proxy.nil? prompt_path = File.expand_path("../prompts/schema_generator.md", __dir__) system_prompt = File.read(prompt_path) diff --git a/spec/unit/chat_spec.rb b/spec/unit/chat_spec.rb index 52b7698..5689ee3 100644 --- a/spec/unit/chat_spec.rb +++ b/spec/unit/chat_spec.rb @@ -5,6 +5,140 @@ RSpec.describe AI::Chat do let(:chat) { AI::Chat.new } + def with_env_var(name, value) + original = ENV.key?(name) ? ENV[name] : :__undefined__ + + if value.nil? + ENV.delete(name) + else + ENV[name] = value + end + + yield + ensure + if original == :__undefined__ + ENV.delete(name) + else + ENV[name] = original + end + end + + def schema_client_double + response = double("response", output_text: '{"type":"object","properties":{},"required":[],"additionalProperties":false}') + responses = double("responses") + allow(responses).to receive(:create).and_return(response) + double("client", responses: responses) + end + + around do |example| + with_env_var("AICHAT_PROXY", nil) do + example.run + end + end + + describe "proxy defaults" do + it "defaults proxy to false when AICHAT_PROXY is not set" do + with_env_var("AICHAT_PROXY", nil) do + client_double = instance_double(OpenAI::Client) + expect(OpenAI::Client).to receive(:new).with(api_key: "test-key").and_return(client_double) + + instance = AI::Chat.new(api_key: "test-key") + + expect(instance.proxy).to be(false) + end + end + + it "defaults proxy to true when AICHAT_PROXY is exactly true" do + with_env_var("AICHAT_PROXY", "true") do + client_double = instance_double(OpenAI::Client) + expect(OpenAI::Client).to receive(:new).with( + api_key: "test-key", + base_url: AI::Chat::BASE_PROXY_URL + ).and_return(client_double) + + instance = AI::Chat.new(api_key: "test-key") + + expect(instance.proxy).to be(true) + end + end + + it "does not enable proxy for non-exact truthy values" do + with_env_var("AICHAT_PROXY", "TRUE") do + client_double = instance_double(OpenAI::Client) + expect(OpenAI::Client).to receive(:new).with(api_key: "test-key").and_return(client_double) + + instance = AI::Chat.new(api_key: "test-key") + + expect(instance.proxy).to be(false) + end + end + + it "allows explicit override to false even when env default is true" do + with_env_var("AICHAT_PROXY", "true") do + client_double = instance_double(OpenAI::Client) + allow(OpenAI::Client).to receive(:new).and_return(client_double) + + instance = AI::Chat.new(api_key: "test-key") + instance.proxy = false + + expect(OpenAI::Client).to have_received(:new).with(api_key: "test-key", base_url: AI::Chat::BASE_PROXY_URL) + expect(OpenAI::Client).to have_received(:new).with(api_key: "test-key") + expect(instance.proxy).to be(false) + end + end + + it "allows explicit override to true when env default is false" do + with_env_var("AICHAT_PROXY", nil) do + client_double = instance_double(OpenAI::Client) + allow(OpenAI::Client).to receive(:new).and_return(client_double) + + instance = AI::Chat.new(api_key: "test-key") + instance.proxy = true + + expect(OpenAI::Client).to have_received(:new).with(api_key: "test-key") + expect(OpenAI::Client).to have_received(:new).with(api_key: "test-key", base_url: AI::Chat::BASE_PROXY_URL) + expect(instance.proxy).to be(true) + end + end + end + + describe ".generate_schema!" do + it "uses env proxy default when proxy keyword is omitted" do + with_env_var("AICHAT_PROXY", "true") do + client_double = schema_client_double + expect(OpenAI::Client).to receive(:new).with( + api_key: "test-key", + base_url: AI::Chat::BASE_PROXY_URL + ).and_return(client_double) + + result = AI::Chat.generate_schema!("A tiny schema", api_key: "test-key", location: false) + + expect(result).to include("\"type\": \"object\"") + end + end + + it "lets explicit proxy false override env proxy default" do + with_env_var("AICHAT_PROXY", "true") do + client_double = schema_client_double + expect(OpenAI::Client).to receive(:new).with(api_key: "test-key").and_return(client_double) + + AI::Chat.generate_schema!("A tiny schema", api_key: "test-key", location: false, proxy: false) + end + end + + it "lets explicit proxy true override env proxy default" do + with_env_var("AICHAT_PROXY", nil) do + client_double = schema_client_double + expect(OpenAI::Client).to receive(:new).with( + api_key: "test-key", + base_url: AI::Chat::BASE_PROXY_URL + ).and_return(client_double) + + AI::Chat.generate_schema!("A tiny schema", api_key: "test-key", location: false, proxy: true) + end + end + end + describe "#add" do it "returns the added message, not the messages array" do result = chat.add("Hello", role: "user")