diff --git a/lib/stripe/api_requestor.rb b/lib/stripe/api_requestor.rb index 40fa42e0e..55ea7727a 100644 --- a/lib/stripe/api_requestor.rb +++ b/lib/stripe/api_requestor.rb @@ -928,6 +928,9 @@ def self.maybe_gc_connection_managers user_agent = "Stripe/#{api_mode} RubyBindings/#{Stripe::VERSION}" user_agent += " " + format_app_info(Stripe.app_info) unless Stripe.app_info.nil? + ai_agent = SystemProfiler.detect_ai_agent + user_agent += " AIAgent/#{ai_agent}" unless ai_agent.empty? + headers = { "User-Agent" => user_agent, "Authorization" => "Bearer #{req_opts[:api_key]}", @@ -1089,6 +1092,23 @@ def self.uname_from_system_ver "uname lookup failed" end + AI_AGENTS = [ + ["ANTIGRAVITY_CLI_ALIAS", "antigravity"], + ["CLAUDECODE", "claude_code"], + ["CLINE_ACTIVE", "cline"], + ["CODEX_SANDBOX", "codex_cli"], + ["CURSOR_AGENT", "cursor"], + ["GEMINI_CLI", "gemini_cli"], + ["OPENCODE", "open_code"], + ].freeze + + def self.detect_ai_agent(env = ENV) + AI_AGENTS.each do |env_var, agent_name| + return agent_name if env[env_var] && !env[env_var].empty? + end + "" + end + def initialize @uname = self.class.uname end @@ -1097,7 +1117,7 @@ def user_agent lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} " \ "(#{RUBY_RELEASE_DATE})" - { + ua = { application: Stripe.app_info, bindings_version: Stripe::VERSION, lang: "ruby", @@ -1108,6 +1128,11 @@ def user_agent uname: @uname, hostname: Socket.gethostname, }.delete_if { |_k, v| v.nil? } + + ai_agent = self.class.detect_ai_agent + ua[:ai_agent] = ai_agent unless ai_agent.empty? + + ua end end diff --git a/test/stripe/api_requestor_test.rb b/test/stripe/api_requestor_test.rb index 1a58c1898..aa6aa5201 100644 --- a/test/stripe/api_requestor_test.rb +++ b/test/stripe/api_requestor_test.rb @@ -797,6 +797,9 @@ class RequestorTest < Test::Unit::TestCase context "app_info" do should "send app_info if set" do old = Stripe.app_info + + APIRequestor::SystemProfiler.stubs(:detect_ai_agent).returns("") + Stripe.set_app_info( "MyAwesomePlugin", partner_id: "partner_1234", @@ -832,6 +835,26 @@ class RequestorTest < Test::Unit::TestCase end end + context "ai_agent" do + should "include AI agent in request headers" do + APIRequestor::SystemProfiler.stubs(:detect_ai_agent).returns("cursor") + + stub_request(:post, "#{Stripe::DEFAULT_API_BASE}/v1/account") + .with do |req| + assert_match(/AIAgent\/cursor$/, req.headers["User-Agent"]) + + data = JSON.parse(req.headers["X-Stripe-Client-User-Agent"]) + assert_equal "cursor", data["ai_agent"] + + true + end.to_return(body: JSON.generate(object: "account")) + + client = APIRequestor.new("sk_test_123") + client.send(request_method, :post, "/v1/account", :api, + &@read_body_chunk_block) + end + end + context "error handling" do should "handle error response with empty body" do stub_request(:post, "#{Stripe::DEFAULT_API_BASE}/v1/charges") @@ -1734,5 +1757,23 @@ class SystemProfilerTest < Test::Unit::TestCase _ = APIRequestor::SystemProfiler.uname_from_system_ver end end + + context ".detect_ai_agent" do + should "detect agent when env var is set" do + assert_equal "claude_code", APIRequestor::SystemProfiler.detect_ai_agent({"CLAUDECODE" => "1"}) + end + + should "return empty string when no agent env vars are set" do + assert_equal "", APIRequestor::SystemProfiler.detect_ai_agent({}) + end + + should "return first matching agent when multiple env vars are set" do + assert_equal "cursor", APIRequestor::SystemProfiler.detect_ai_agent({"CURSOR_AGENT" => "1", "OPENCODE" => "1"}) + end + + should "ignore empty string env vars" do + assert_equal "", APIRequestor::SystemProfiler.detect_ai_agent({"CLAUDECODE" => ""}) + end + end end end