Skip to content

Commit 596c603

Browse files
Add agent information to UserAgent (#1743)
* Add agent information to UserAgent * format
1 parent e4b87f0 commit 596c603

2 files changed

Lines changed: 62 additions & 1 deletion

File tree

stripe/_api_requestor.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from io import BytesIO, IOBase
22
import json
3+
import os
34
import platform
45
from typing import (
56
Any,
@@ -469,6 +470,23 @@ def specific_oauth_error(self, rbody, rcode, resp, rheaders, error_code):
469470

470471
return None
471472

473+
AI_AGENTS = [
474+
("ANTIGRAVITY_CLI_ALIAS", "antigravity"),
475+
("CLAUDECODE", "claude_code"),
476+
("CLINE_ACTIVE", "cline"),
477+
("CODEX_SANDBOX", "codex_cli"),
478+
("CURSOR_AGENT", "cursor"),
479+
("GEMINI_CLI", "gemini_cli"),
480+
("OPENCODE", "open_code"),
481+
]
482+
483+
@staticmethod
484+
def _detect_ai_agent(environ: Mapping[str, str]) -> str:
485+
for env_var, agent_name in _APIRequestor.AI_AGENTS:
486+
if environ.get(env_var):
487+
return agent_name
488+
return ""
489+
472490
def request_headers(
473491
self, method: HttpVerb, api_mode: ApiMode, options: RequestOptions
474492
):
@@ -479,6 +497,10 @@ def request_headers(
479497
if stripe.app_info:
480498
user_agent += " " + self._format_app_info(stripe.app_info)
481499

500+
agent = self._detect_ai_agent(os.environ)
501+
if agent:
502+
user_agent += " AIAgent/" + agent
503+
482504
ua: Dict[str, Union[str, "AppInfo"]] = {
483505
"bindings_version": VERSION,
484506
"lang": "python",
@@ -497,6 +519,8 @@ def request_headers(
497519
ua[attr] = val
498520
if stripe.app_info:
499521
ua["application"] = stripe.app_info
522+
if agent:
523+
ua["ai_agent"] = agent
500524

501525
headers: Dict[str, str] = {
502526
"X-Stripe-Client-User-Agent": json.dumps(ua),

tests/test_api_requestor.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,8 @@ def test_sets_default_http_client(self, mocker):
671671
# the newly created client is reused
672672
assert stripe.default_http_client == new_default_client
673673

674-
def test_uses_app_info(self, requestor, http_client_mock):
674+
def test_uses_app_info(self, requestor, mocker, http_client_mock):
675+
mocker.patch.object(_APIRequestor, "_detect_ai_agent", return_value="")
675676
try:
676677
old = stripe.app_info
677678
stripe.set_app_info(
@@ -707,6 +708,42 @@ def test_uses_app_info(self, requestor, http_client_mock):
707708
finally:
708709
stripe.app_info = old
709710

711+
def test_detect_ai_agent(self):
712+
assert (
713+
_APIRequestor._detect_ai_agent({"CLAUDECODE": "1"})
714+
== "claude_code"
715+
)
716+
717+
def test_detect_ai_agent_no_env_vars(self):
718+
assert _APIRequestor._detect_ai_agent({}) == ""
719+
720+
def test_detect_ai_agent_first_match_wins(self):
721+
assert (
722+
_APIRequestor._detect_ai_agent(
723+
{"CURSOR_AGENT": "1", "OPENCODE": "1"}
724+
)
725+
== "cursor"
726+
)
727+
728+
def test_ai_agent_included_in_request_headers(
729+
self, requestor, mocker, http_client_mock
730+
):
731+
mocker.patch.object(
732+
_APIRequestor, "_detect_ai_agent", return_value="cursor"
733+
)
734+
http_client_mock.stub_request(
735+
"get", path=self.v1_path, rbody="{}", rcode=200
736+
)
737+
requestor.request("get", self.v1_path, {}, base_address="api")
738+
739+
last_call = http_client_mock.get_last_call()
740+
ua = last_call.get_raw_header("User-Agent")
741+
assert ua.endswith(" AIAgent/cursor")
742+
client_ua = json.loads(
743+
last_call.get_raw_header("X-Stripe-Client-User-Agent")
744+
)
745+
assert client_ua["ai_agent"] == "cursor"
746+
710747
def test_handles_failed_platform_call(
711748
self, requestor, mocker, http_client_mock
712749
):

0 commit comments

Comments
 (0)