From 2a6a40590916e3799c4b406c0640a9c32ff72e95 Mon Sep 17 00:00:00 2001 From: Jelani Woods Date: Wed, 28 Jan 2026 12:20:42 -0600 Subject: [PATCH 1/6] Refactor to set base_url for client --- lib/ai/chat.rb | 253 ++++++++++++++----------------------------------- 1 file changed, 72 insertions(+), 181 deletions(-) diff --git a/lib/ai/chat.rb b/lib/ai/chat.rb index 9d1d49c..a916dc7 100644 --- a/lib/ai/chat.rb +++ b/lib/ai/chat.rb @@ -23,19 +23,19 @@ class Chat include AI::Http # :reek:Attribute - attr_accessor :background, :code_interpreter, :conversation_id, :image_generation, :image_folder, :messages, :model, :proxy, :reasoning_effort, :web_search - attr_reader :client, :last_response_id, :schema, :schema_file + attr_accessor :background, :code_interpreter, :conversation_id, :image_generation, :image_folder, :messages, :model, :reasoning_effort, :web_search + attr_reader :client, :last_response_id, :proxy, :schema, :schema_file - PROXY_URL = "https://prepend.me/" + 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) + @proxy = false @messages = [] @reasoning_effort = nil @model = "gpt-5.2" @client = OpenAI::Client.new(api_key: @api_key) @last_response_id = nil - @proxy = false @image_generation = false @image_folder = "./images" end @@ -45,34 +45,25 @@ def self.generate_schema!(description, location: "schema.json", api_key: nil, ap prompt_path = File.expand_path("../prompts/schema_generator.md", __dir__) system_prompt = File.read(prompt_path) - json = if proxy - uri = URI(PROXY_URL + "api.openai.com/v1/responses") - parameters = { - model: "gpt-5.2", - input: [ - {role: :system, content: system_prompt}, - {role: :user, content: description} - ], - text: {format: {type: "json_object"}}, - reasoning: {effort: "high"} - } + options = { + api_key: api_key, + base_url: proxy ? BASE_PROXY_URL : nil + }.compact - send_request(uri, content_type: "json", parameters: parameters, method: "post") - else - client = OpenAI::Client.new(api_key: api_key) - response = client.responses.create( - model: "gpt-5.2", - input: [ - {role: :system, content: system_prompt}, - {role: :user, content: description} - ], - text: {format: {type: "json_object"}}, - reasoning: {effort: "high"} - ) + client = OpenAI::Client.new(**options) + response = client.responses.create( + model: "gpt-5.2", + input: [ + {role: :system, content: system_prompt}, + {role: :user, content: description} + ], + text: {format: {type: "json_object"}}, + reasoning: {effort: "high"} + ) + + output_text = response.output_text + json = JSON.parse(output_text) - output_text = response.output_text - JSON.parse(output_text) - end content = JSON.pretty_generate(json) if location path = Pathname.new(location) @@ -167,6 +158,14 @@ def get_response(wait: false, timeout: 600) parse_response(response) end + def proxy=(value) + @proxy = value + @client = OpenAI::Client.new( + api_key: @api_key, + base_url: BASE_PROXY_URL + ) + end + def schema=(value) if value.is_a?(String) parsed = JSON.parse(value, symbolize_names: true) @@ -191,29 +190,7 @@ def last def get_items(order: :asc) raise "No conversation_id set. Call generate! first to create a conversation." unless conversation_id - raw_items = if proxy - uri = URI(PROXY_URL + "api.openai.com/v1/conversations/#{conversation_id}/items?order=#{order}") - response_hash = send_request(uri, content_type: "json", method: "get") - - if response_hash.key?(:data) - response_hash.dig(:data).map do |hash| - # Transform values to allow expected symbols that non-proxied request returns - - hash.transform_values! do |value| - if hash.key(value) == :type - value.to_sym - else - value - end - end - end - response_hash - end - # Convert to Struct to allow same interface as non-proxied request - create_deep_struct(response_hash) - else - client.conversations.items.list(conversation_id, order: order) - end + raw_items = client.conversations.items.list(conversation_id, order: order) Items.new(raw_items, conversation_id: conversation_id) end @@ -280,14 +257,8 @@ def extract_filename(obj) end def create_conversation - self.conversation_id = if proxy - uri = URI(PROXY_URL + "api.openai.com/v1/conversations") - response = send_request(uri, content_type: "json", method: "post") - response.dig(:id) - else - conversation = client.conversations.create - conversation.id - end + conversation = client.conversations.create + self.conversation_id = conversation.id end # :reek:TooManyStatements @@ -307,50 +278,22 @@ def create_response messages_to_send = prepare_messages_for_api parameters[:input] = strip_responses(messages_to_send) unless messages_to_send.empty? - if proxy - uri = URI(PROXY_URL + "api.openai.com/v1/responses") - send_request(uri, content_type: "json", parameters: parameters, method: "post") - else - client.responses.create(**parameters) - end + client.responses.create(**parameters) end # :reek:NilCheck # :reek:TooManyStatements def parse_response(response) - if proxy && response.is_a?(Hash) - response_messages = response.dig(:output).select do |output| - output.dig(:type) == "message" - end - - message_contents = response_messages.map do |message| - message.dig(:content) - end.flatten + text_response = response.output_text + response_id = response.id + response_status = response.status + response_model = response.model + response_usage = response.usage.to_h.slice(:input_tokens, :output_tokens, :total_tokens) - output_texts = message_contents.select do |content| - content[:type] == "output_text" - end - - text_response = output_texts.map { |output| output[:text] }.join - response_id = response.dig(:id) - response_status = response.dig(:status).to_sym - response_model = response.dig(:model) - response_usage = response.dig(:usage)&.slice(:input_tokens, :output_tokens, :total_tokens) - - if response.key?(:conversation) - self.conversation_id = response.dig(:conversation, :id) - end - else - text_response = response.output_text - response_id = response.id - response_status = response.status - response_model = response.model - response_usage = response.usage.to_h.slice(:input_tokens, :output_tokens, :total_tokens) - - if response.conversation - self.conversation_id = response.conversation.id - end + if response.conversation + self.conversation_id = response.conversation.id end + image_filenames = extract_and_save_images(response) + extract_and_save_files(response) chat_response = { @@ -579,30 +522,20 @@ def wrap_schema_if_needed(schema) def extract_and_save_images(response) image_filenames = [] - image_outputs = if proxy - response.dig(:output).select { |output| - output.dig(:type) == "image_generation_call" - } - else - response.output.select { |output| + image_outputs = response.output.select { |output| output.respond_to?(:type) && output.type == :image_generation_call } - end return image_filenames if image_outputs.empty? - response_id = proxy ? response.dig(:id) : response.id + response_id = response.id subfolder_path = create_images_folder(response_id) image_outputs.each_with_index do |output, index| - if proxy - next unless output.key?(:result) && output.dig(:result) - else - next unless output.respond_to?(:result) && output.result - end + next unless output.respond_to?(:result) && output.result warn_if_file_fails_to_save do - result = proxy ? output.dig(:result) : output.result + result = output.result image_data = Base64.strict_decode64(result) filename = "#{(index + 1).to_s.rjust(3, "0")}.png" @@ -664,72 +597,40 @@ def validate_api_key def extract_and_save_files(response) filenames = [] - if proxy - message_outputs = response.dig(:output).select do |output| - output.dig(:type) == "message" - end + message_outputs = response.output.select do |output| + output.respond_to?(:type) && output.type == :message + end - outputs_with_annotations = message_outputs.map do |message| - message.dig(:content).find do |content| - content.dig(:annotations).length.positive? - end - end.compact - else - message_outputs = response.output.select do |output| - output.respond_to?(:type) && output.type == :message + outputs_with_annotations = message_outputs.map do |message| + message.content.find do |content| + content.respond_to?(:annotations) && content.annotations.length.positive? end - - outputs_with_annotations = message_outputs.map do |message| - message.content.find do |content| - content.respond_to?(:annotations) && content.annotations.length.positive? - end - end.compact - end + end.compact return filenames if outputs_with_annotations.empty? - response_id = proxy ? response.dig(:id) : response.id + response_id = response.id subfolder_path = create_images_folder(response_id) - if proxy - annotations = outputs_with_annotations.map do |output| - output.dig(:annotations).find do |annotation| - annotation.key?(:filename) - end - end.compact - - annotations.each do |annotation| - container_id = annotation.dig(:container_id) - file_id = annotation.dig(:file_id) - filename = annotation.dig(:filename) - - warn_if_file_fails_to_save do - file_content = retrieve_file(file_id, container_id: container_id) - file_path = File.join(subfolder_path, filename) - File.binwrite(file_path, file_content) - filenames << file_path - end + annotations = outputs_with_annotations.map do |output| + output.annotations.find do |annotation| + annotation.respond_to?(:filename) end - else - annotations = outputs_with_annotations.map do |output| - output.annotations.find do |annotation| - annotation.respond_to?(:filename) - end - end.compact - - annotations.each do |annotation| - container_id = annotation.container_id - file_id = annotation.file_id - filename = annotation.filename - - warn_if_file_fails_to_save do - file_content = retrieve_file(file_id, container_id: container_id) - file_path = File.join(subfolder_path, filename) - File.binwrite(file_path, file_content.read) - filenames << file_path - end + end.compact + + annotations.each do |annotation| + container_id = annotation.container_id + file_id = annotation.file_id + filename = annotation.filename + + warn_if_file_fails_to_save do + file_content = retrieve_file(file_id, container_id: container_id) + file_path = File.join(subfolder_path, filename) + File.binwrite(file_path, file_content.read) + filenames << file_path end end + filenames end @@ -790,22 +691,12 @@ def wait_for_response(timeout) end def retrieve_response(response_id) - if proxy - uri = URI(PROXY_URL + "api.openai.com/v1/responses/#{response_id}") - send_request(uri, content_type: "json", method: "get") - else - client.responses.retrieve(response_id) - end + client.responses.retrieve(response_id) end def retrieve_file(file_id, container_id: nil) - if proxy - uri = URI(PROXY_URL + "api.openai.com/v1/containers/#{container_id}/files/#{file_id}/content") - send_request(uri, method: "get") - else - container_content = client.containers.files.content - container_content.retrieve(file_id, container_id: container_id) - end + container_content = client.containers.files.content + container_content.retrieve(file_id, container_id: container_id) end end end From 2391d051699927f6fc549ad36155285239bb8082 Mon Sep 17 00:00:00 2001 From: Jelani Woods Date: Wed, 28 Jan 2026 12:22:23 -0600 Subject: [PATCH 2/6] remove http module --- lib/ai/chat.rb | 4 ---- lib/ai/http.rb | 45 --------------------------------------------- 2 files changed, 49 deletions(-) delete mode 100644 lib/ai/http.rb diff --git a/lib/ai/chat.rb b/lib/ai/chat.rb index a916dc7..78290ba 100644 --- a/lib/ai/chat.rb +++ b/lib/ai/chat.rb @@ -11,8 +11,6 @@ require "tty-spinner" require "timeout" -require_relative "http" - module AI # :reek:MissingSafeMethod { exclude: [ generate! ] } # :reek:TooManyMethods @@ -20,8 +18,6 @@ module AI # :reek:InstanceVariableAssumption # :reek:IrresponsibleModule class Chat - include AI::Http - # :reek:Attribute attr_accessor :background, :code_interpreter, :conversation_id, :image_generation, :image_folder, :messages, :model, :reasoning_effort, :web_search attr_reader :client, :last_response_id, :proxy, :schema, :schema_file diff --git a/lib/ai/http.rb b/lib/ai/http.rb deleted file mode 100644 index f47f353..0000000 --- a/lib/ai/http.rb +++ /dev/null @@ -1,45 +0,0 @@ -require "net/http" -module AI - module Http - def send_request(uri, method:, content_type: nil, parameters: nil) - Net::HTTP.start(uri.host, 443, use_ssl: true) do |http| - headers = { - "Authorization" => "Bearer #{@api_key}" - } - if content_type - headers.store("Content-Type", "application/json") - end - net_http_method = "Net::HTTP::#{method.downcase.capitalize}" - client = Kernel.const_get(net_http_method) - request = client.new(uri, headers) - - if parameters - request.body = parameters.to_json - end - response = http.request(request) - - # Handle proxy server 503 HTML response - begin - if content_type - return JSON.parse(response.body, symbolize_names: true) - else - return response.body - end - rescue JSON::ParserError, TypeError => e - raise JSON::ParserError, "Failed to parse response from proxy: #{e.message}" - end - end - end - - def create_deep_struct(value) - case value - when Hash - OpenStruct.new(value.transform_values { |hash_value| send __method__, hash_value }) - when Array - value.map { |element| send __method__, element } - else - value - end - end - end -end From dade920b491988118d2339f254040bb9512210c3 Mon Sep 17 00:00:00 2001 From: Jelani Woods Date: Wed, 28 Jan 2026 12:57:27 -0600 Subject: [PATCH 3/6] Fix schema example method call --- examples/14_schema_generation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/14_schema_generation.rb b/examples/14_schema_generation.rb index 80e3b7f..88e4bfd 100644 --- a/examples/14_schema_generation.rb +++ b/examples/14_schema_generation.rb @@ -108,7 +108,7 @@ puts chat = AI::Chat.new chat.schema_file = "schema.json" -if chat.schema.present? +if chat.schema.is_a?(Hash) puts "✓ AI::Chat#schema_file= assigns AI::Chat#schema" else puts "✗ AI::Chat#schema_file= does not assign AI::Chat#schema" From c6649de48a5dfd917ce632d549d867a019aa6898 Mon Sep 17 00:00:00 2001 From: Jelani Woods Date: Wed, 28 Jan 2026 13:24:00 -0600 Subject: [PATCH 4/6] Add example of multiple system messages --- .../13_conversation_features_comprehensive.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/examples/13_conversation_features_comprehensive.rb b/examples/13_conversation_features_comprehensive.rb index 4e2b351..4dfd29d 100644 --- a/examples/13_conversation_features_comprehensive.rb +++ b/examples/13_conversation_features_comprehensive.rb @@ -138,6 +138,22 @@ puts "Response from loaded conversation: #{chat2.last[:content]}" puts +puts "8. Creating multiple system messages:" +chat = AI::Chat.new +chat.web_search = true +puts "Message after first system message:" +chat.system("You speak like spider-man.") +chat.user("Where is the best place to get pizza in Chicago?") +chat.generate! +puts "Response: #{chat.last[:content]}" +puts "\n\n" +chat.system("End every sentence with ✨") +chat.generate! +puts "Message after second system message:" +puts "Response: #{chat.last[:content]}" +puts + puts "=" * 60 puts "All conversation features demonstrated!" puts "=" * 60 + From 931a48b1ba4ca3bbd1a9574204c00494c62c2715 Mon Sep 17 00:00:00 2001 From: Jelani Woods Date: Wed, 28 Jan 2026 13:54:36 -0600 Subject: [PATCH 5/6] rename var --- .../13_conversation_features_comprehensive.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/13_conversation_features_comprehensive.rb b/examples/13_conversation_features_comprehensive.rb index 4dfd29d..c363924 100644 --- a/examples/13_conversation_features_comprehensive.rb +++ b/examples/13_conversation_features_comprehensive.rb @@ -139,18 +139,18 @@ puts puts "8. Creating multiple system messages:" -chat = AI::Chat.new -chat.web_search = true +chat3 = AI::Chat.new +chat3.web_search = true puts "Message after first system message:" -chat.system("You speak like spider-man.") -chat.user("Where is the best place to get pizza in Chicago?") -chat.generate! -puts "Response: #{chat.last[:content]}" +chat3.system("You speak like spider-man.") +chat3.user("Where is the best place to get pizza in Chicago?") +chat3.generate! +puts "Response: #{chat3.last[:content]}" puts "\n\n" -chat.system("End every sentence with ✨") -chat.generate! +chat3.system("End every sentence with ✨") +chat3.generate! puts "Message after second system message:" -puts "Response: #{chat.last[:content]}" +puts "Response: #{chat3.last[:content]}" puts puts "=" * 60 From a500c8f5916bfd22500b5a2377311d75b0fc7bdc Mon Sep 17 00:00:00 2001 From: Jelani Woods Date: Wed, 28 Jan 2026 14:57:09 -0600 Subject: [PATCH 6/6] toggle @client on proxy toggle --- lib/ai/chat.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/ai/chat.rb b/lib/ai/chat.rb index 78290ba..94f1ffe 100644 --- a/lib/ai/chat.rb +++ b/lib/ai/chat.rb @@ -156,10 +156,15 @@ def get_response(wait: false, timeout: 600) def proxy=(value) @proxy = value - @client = OpenAI::Client.new( + if value + @client = OpenAI::Client.new( api_key: @api_key, base_url: BASE_PROXY_URL ) + else + @client = OpenAI::Client.new(api_key: @api_key) + end + value end def schema=(value)